diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 8c1545286fe9..c131c80cd735 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -30,7 +30,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' coverage: none # Build the latest User Guide diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 0e1df16d4ef5..af1fd2f5b6c1 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -63,6 +63,8 @@ jobs: vendor/bin/phpcov merge --clover build/logs/clover.xml build/cov - name: Upload coverage to Coveralls - run: vendor/bin/php-coveralls --verbose --exclude-no-stmt --ansi + run: | + composer global require php-coveralls/php-coveralls + php-coveralls --verbose --exclude-no-stmt --ansi env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 5461dda461bd..f2ecb8effbad 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -28,8 +28,8 @@ jobs: fail-fast: false matrix: php-version: - - '7.4' - - '8.0' + - '8.1' + - '8.2' steps: - name: Checkout diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index 04ef53bb99c5..61093d449a93 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -41,7 +41,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' tools: phpcpd extensions: dom, mbstring @@ -57,4 +57,6 @@ jobs: --exclude system/Debug/Exceptions.php --exclude system/HTTP/SiteURI.php --exclude system/Validation/Rules.php + --exclude system/Autoloader/Autoloader.php + --exclude system/Config/Filters.php -- app/ public/ system/ diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index 53124f695162..afc6d56ace2c 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -47,7 +47,7 @@ jobs: steps: - id: coverage-php-version run: | - echo "version=8.1" >> $GITHUB_OUTPUT + echo "version=8.2" >> $GITHUB_OUTPUT sanity-tests: name: Others @@ -56,8 +56,6 @@ jobs: strategy: matrix: php-version: - - '7.4' - - '8.0' - '8.1' - '8.2' - '8.3' @@ -87,8 +85,6 @@ jobs: fail-fast: false matrix: php-version: - - '7.4' - - '8.0' - '8.1' - '8.2' - '8.3' @@ -101,7 +97,7 @@ jobs: mysql-version: - '8.0' include: - - php-version: '7.4' + - php-version: '8.1' db-platform: MySQLi mysql-version: '5.7' - php-version: '8.3' @@ -130,8 +126,6 @@ jobs: strategy: matrix: php-version: - - '7.4' - - '8.0' - '8.1' - '8.2' - '8.3' @@ -160,8 +154,6 @@ jobs: strategy: matrix: php-version: - - '7.4' - - '8.0' - '8.1' - '8.2' - '8.3' diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index bb03e62ed161..286ca76cab42 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.4', '8.0'] + php-versions: ['8.1', '8.3'] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 74c6d0ce6c4f..71cf213fbfaf 100644 --- a/.gitignore +++ b/.gitignore @@ -126,5 +126,6 @@ nb-configuration.xml /results/ /phpunit*.xml /.phpunit.*.cache +/.phpunit.cache /.php-cs-fixer.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 3653cf1349c2..807cd0078e7d 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -56,13 +56,10 @@ 'admin@codeigniter.com' ); -// @TODO: remove this check when support for PHP 7.4 is dropped -if (PHP_VERSION_ID >= 80000) { - $config - ->registerCustomFixers(FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer')) - ->setRules(array_merge($config->getRules(), [ - NoCodeSeparatorCommentFixer::name() => true, - ])); -} +$config + ->registerCustomFixers(FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer')) + ->setRules(array_merge($config->getRules(), [ + NoCodeSeparatorCommentFixer::name() => true, + ])); return $config; diff --git a/.php-cs-fixer.no-header.php b/.php-cs-fixer.no-header.php index 7c9ae1e80737..3729d434acba 100644 --- a/.php-cs-fixer.no-header.php +++ b/.php-cs-fixer.no-header.php @@ -38,13 +38,10 @@ $config = Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); -// @TODO: remove this check when support for PHP 7.4 is dropped -if (PHP_VERSION_ID >= 80000) { - $config - ->registerCustomFixers(FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer')) - ->setRules(array_merge($config->getRules(), [ - NoCodeSeparatorCommentFixer::name() => true, - ])); -} +$config + ->registerCustomFixers(FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer')) + ->setRules(array_merge($config->getRules(), [ + NoCodeSeparatorCommentFixer::name() => true, + ])); return $config; diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php index fe634f0a6f21..9f9f9b79a464 100644 --- a/.php-cs-fixer.user-guide.php +++ b/.php-cs-fixer.user-guide.php @@ -48,13 +48,10 @@ $config = Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); -// @TODO: remove this check when support for PHP 7.4 is dropped -if (PHP_VERSION_ID >= 80000) { - $config - ->registerCustomFixers(FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer')) - ->setRules(array_merge($config->getRules(), [ - NoCodeSeparatorCommentFixer::name() => true, - ])); -} +$config + ->registerCustomFixers(FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer')) + ->setRules(array_merge($config->getRules(), [ + NoCodeSeparatorCommentFixer::name() => true, + ])); return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md index f0147f72b57b..60950f4cf0f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,137 @@ # Changelog +## [v4.5.0](https://github.com/codeigniter4/CodeIgniter4/tree/v4.5.0) (2024-04-07) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.4.8...v4.5.0) + +### Breaking Changes + +* refactor: always use multiple filters by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7903 +* fix: update psr/log to v2 and fix Logger interface by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7967 +* fix: incorrect return type for Model::objectToRawArray() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7986 +* fix: filter exec order by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7955 +* refactor: Remove deprecated Config\Config by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8016 +* fix: `FileLocator::findQualifiedNameFromPath()` behavior by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8010 +* refactor: remove deprecated methods in Model by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8032 +* fix: route options are not merged (outer filter is merged with inner filter) by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8033 +* fix: route options are not merged (inner filter overrides outer filter) by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7981 +* feat: FileLocator caching by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8017 +* refactor: remove deprecated properties and methods in CodeIgniter class by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8050 +* fix: make Factories final by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8056 +* refactor: remove deprecated test classes by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8057 +* refactor: make IncomingRequest::$uri protected by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8067 +* refactor: remove deprecated spark commands by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8086 +* refactor: remove deprecated Request::isValidIP() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8090 +* fix: set_cookie() $expire type by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8080 +* fix: remove traditional validation rule param types (1/2) by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8078 +* fix: filters are executed when controller does not exist with Auto Routing (Legacy). by @ping-yee in https://github.com/codeigniter4/CodeIgniter4/pull/7925 +* fix: remove traditional validation rule param types (2/2) by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8124 +* refactor: remove deprecated ModelFactory by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8139 +* refactor: remove deprecated properties in Response by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8142 +* fix: remove deprecated upper functionality in `Request::getMethod()` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8186 +* feat: new Required Filters by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8053 +* refactor: remove deprecated CastException exception by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8469 +* refactor: remove deprecated MockSecurityConfig by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8472 +* refactor: remove deprecated CodeIgniter\Entity by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8497 +* refactor: remove deprecated Cache\Exceptions\ExceptionInterface by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8498 +* fix: API\ResponseTrait can't return string as JSON by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8490 +* feat: Validation::run() accepts DB connection by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8499 +* feat: 404 Override sets 404 by default by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8535 +* refactor: remove deprecated const SPARKED by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8559 +* refactor: remove deprecated BaseService::discoverServices() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8589 +* fix: move Kint loading to Autoloader by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8603 +* feat: add Boot class by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8604 + +### Fixed Bugs + +* fix: error on `Config\Kint` with Config Caching by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8003 +* fix: route key lowercase HTTP verbs by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8235 +* fix: use `addHeader()` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8240 +* fix: QueryBuilder limit(0) bug by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8280 +* fix: SQLite3 may not throw DatabaseException by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8467 +* [4.5] fix: DEBUG-VIEW comments are not output by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8523 +* [4.5] fix: $db->dateFormat merge by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8539 +* [4.5] fix: spark does not work with composer install --no-dev by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8534 +* [4.5] fix: Composer autoload.psr4 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8569 +* [4.5] fix: errors when not updating Config\Feature by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8570 +* [4.5] fix: TypeError in Filters by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8683 + +### New Features + +* feat: Language translations finder and update by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/7896 + +### Enhancements + +* feat: domparser - ability to write more advanced expressions by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/7946 +* feat: [Validation] Callable Rules by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7933 +* perf: autoloader by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8005 +* feat: db:table shows db config by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7972 +* feat: add `{memory_usage}` replacement by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8008 +* perf: replace $locator->getClassname() with findQualifiedNameFromPath() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8012 +* feat: add Method/Route logging in exceptionHandler() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8108 +* feat: add `config:check` command to check Config vaules by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8110 +* feat: one generator command could have multiple views by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8119 +* feat: improve CLI input testability by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7978 +* feat: add ArrayHelper::dotKeyExists() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8131 +* feat: add CSP clearDirective() to clear existing directive by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8220 +* feat: [Validation] add `field_exists` rule by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8123 +* feat: add Message::addHeader() to add header with the same name by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8194 +* feat: `spark filter:check` shows "Required Filters" by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8236 +* feat: [Commands] `lang:find` show bad keys when scanning (v2) by @neznaika0 in https://github.com/codeigniter4/CodeIgniter4/pull/8285 +* feat: add `--dbgroup` option to `spark db:table` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8292 +* feat: [Auto Routing Improved] add option to translate uri to camel case by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8321 +* feat: `spark routes` shows "Required Filters" by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8237 +* feat: HTTP method-aware web page caching by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8364 +* feat: `spark make:test` creates test files in `/tests/` directory v2 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8388 +* feat: [Routing] add option to pass multiple URI segments to one Controller parameter by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8348 +* feat: add DataConverter to convert types by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8230 +* feat: [Model] add option $updateOnlyChanged by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8455 +* feat: add event points for spark commands by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8496 +* feat: 404 controller also can get PageNotFoundException message by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8491 +* feat: add DB config `dateFormat` to provide default date/time formats by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8525 +* feat: use $db->dateFormat in Model by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8538 +* feat: permit __invoke() method as Controller default method by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8533 +* feat: add Model field casting by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8243 +* feat: add spark command to check php.ini by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8581 +* feat: improve Redis Session by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8578 +* feat: add Config\Optimize by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8605 +* feat: support database name with dots by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8664 +* feat: add `spark optimize` command by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8610 +* feat: add CORS filter by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8649 +* feat: Support faker modifiers on Fabricator by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/8671 +* feat: environment-specific Config\Security::$redirect by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8673 +* feat: `spark config:check` detects Config Caching by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8711 + +### Refactoring + +* Drop PHP 7.4 support by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/7924 +* [4.5] refactor: remove unused `use` in Model by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8045 +* [4.5] refactor: remove BaseModel assert() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8046 +* [4.5] refactor: Filters by rector by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8071 +* perf: defer instantiation of Validation in Model by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8087 +* refactor: fix types by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8091 +* refactor: move ArrayHelper class by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8130 +* [4.5] refactor: fix types by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8138 +* refactor: fix param types by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8175 +* refactor: Validation rule field_exists by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8242 +* refactor: `TestResponse` is now a class of its own by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/8264 +* refactor: fix TypeError in strict mode by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8270 +* refactor: add `declare(strict_types=1)` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8072 +* refactor: remove deprecated Controller::loadHelpers() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8286 +* refactor: remove deprecated methods in Security by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8287 +* refactor: HTTP verbs in Router by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8317 +* refactor: remove unused exception classes by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8468 +* [4.5] refactor: add `declare(strict_types=1)` to ForgeModifyColumnTest by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8502 +* [4.5] refactor: use local variables in Model by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8565 +* refactor: remove unnecessary BaseService::$services assignment by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8609 +* perf: add Factories::get() v2 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8600 +* perf: add Services::get() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8607 +* refactor: remove deprecated items in Request by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8617 +* refactor: followup performance `service()` by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/8623 +* [4.5] refactor: add declare(strict_types=1) in BadRequestException by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8682 +* refactor: DB config properties by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8693 +* refactor: upgrade to PHP 8.1 with rector by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8354 +* refactor: update PHPUnit to 10 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8069 + ## [v4.4.8](https://github.com/codeigniter4/CodeIgniter4/tree/v4.4.8) (2024-04-07) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.4.7...v4.4.8) diff --git a/README.md b/README.md index 1508d10b28a6..6fbe0b4fa16c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Made with [contrib.rocks](https://contrib.rocks). ## Server Requirements -PHP version 7.4 or higher is required, with the following extensions installed: +PHP version 8.1 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) - [mbstring](http://php.net/manual/en/mbstring.installation.php) diff --git a/admin/framework/README.md b/admin/framework/README.md index 30843d0d9716..32668f5af19e 100644 --- a/admin/framework/README.md +++ b/admin/framework/README.md @@ -42,7 +42,7 @@ Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/ ## Server Requirements -PHP version 7.4 or higher is required, with the following extensions installed: +PHP version 8.1 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) - [mbstring](http://php.net/manual/en/mbstring.installation.php) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 891bff556031..d46c7490ec56 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -10,12 +10,11 @@ "slack": "https://codeigniterchat.slack.com" }, "require": { - "php": "^7.4 || ^8.0", + "php": "^8.1", "ext-intl": "*", - "ext-json": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.9", - "psr/log": "^1.1" + "laminas/laminas-escaper": "^2.13", + "psr/log": "^3.0" }, "require-dev": { "codeigniter/coding-standard": "^1.7", @@ -24,7 +23,7 @@ "kint-php/kint": "^5.0.4", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", - "phpunit/phpunit": "^9.1", + "phpunit/phpunit": "^10.5.16", "predis/predis": "^1.1 || ^2.0" }, "suggest": { diff --git a/admin/framework/phpunit.xml.dist b/admin/framework/phpunit.xml.dist index 51c5fddc530b..0235b8a739c1 100644 --- a/admin/framework/phpunit.xml.dist +++ b/admin/framework/phpunit.xml.dist @@ -1,51 +1,33 @@ - - - - ./app - - - ./app/Views - ./app/Config/Routes.php - - - - - - - - - - - ./tests - - - - - - - - - - - - - - - - - + + + + + + + - + + + + ./app + + + ./app/Views + ./app/Config/Routes.php + + diff --git a/admin/starter/.github/workflows/phpunit.yml b/admin/starter/.github/workflows/phpunit.yml index 73d413cad92f..f43435df9765 100644 --- a/admin/starter/.github/workflows/phpunit.yml +++ b/admin/starter/.github/workflows/phpunit.yml @@ -2,7 +2,7 @@ name: PHPUnit on: pull_request: - branches: + branches: - develop jobs: @@ -11,7 +11,7 @@ jobs: strategy: matrix: - php-versions: ['7.4', '8.0'] + php-versions: ['8.1', '8.3'] runs-on: ubuntu-latest diff --git a/admin/starter/README.md b/admin/starter/README.md index 976259fc14c1..43eb3718978e 100644 --- a/admin/starter/README.md +++ b/admin/starter/README.md @@ -50,7 +50,7 @@ Problems with it can be raised on our forum, or as issues in the main repository ## Server Requirements -PHP version 7.4 or higher is required, with the following extensions installed: +PHP version 8.1 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) - [mbstring](http://php.net/manual/en/mbstring.installation.php) diff --git a/admin/starter/composer.json b/admin/starter/composer.json index 082d7b648c25..0b3a889757bc 100644 --- a/admin/starter/composer.json +++ b/admin/starter/composer.json @@ -10,15 +10,18 @@ "slack": "https://codeigniterchat.slack.com" }, "require": { - "php": "^7.4 || ^8.0", + "php": "^8.1", "codeigniter4/framework": "^4.0" }, "require-dev": { "fakerphp/faker": "^1.9", "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^9.1" + "phpunit/phpunit": "^10.5.16" }, "autoload": { + "psr-4": { + "App\\": "app/" + }, "exclude-from-classmap": [ "**/Database/Migrations/**" ] diff --git a/admin/starter/phpunit.xml.dist b/admin/starter/phpunit.xml.dist index 62a473a733c4..7cd6d3af600a 100644 --- a/admin/starter/phpunit.xml.dist +++ b/admin/starter/phpunit.xml.dist @@ -1,51 +1,33 @@ - - - - ./app - - - ./app/Views - ./app/Config/Routes.php - - - - - - - - - - - ./tests - - - - - - - - - - - - - - - - - + + + + + + + - + + + + ./app + + + ./app/Views + ./app/Config/Routes.php + + diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index 7a4602d72920..76cd9263556e 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -30,22 +30,17 @@ class Autoload extends AutoloadConfig * their location on the file system. These are used by the autoloader * to locate files the first time they have been instantiated. * - * The '/app' and '/system' directories are already mapped for you. - * you may change the name of the 'App' namespace if you wish, + * The 'Config' (APPPATH . 'Config') and 'CodeIgniter' (SYSTEMPATH) are + * already mapped for you. + * + * You may change the name of the 'App' namespace if you wish, * but this should be done prior to creating any namespaced classes, * else you will need to modify all of those classes for this to work. * - * Prototype: - * $psr4 = [ - * 'CodeIgniter' => SYSTEMPATH, - * 'App' => APPPATH - * ]; - * * @var array|string> */ public $psr4 = [ - APP_NAMESPACE => APPPATH, // For custom app namespace - 'Config' => APPPATH . 'Config', + APP_NAMESPACE => APPPATH, ]; /** diff --git a/app/Config/Boot/production.php b/app/Config/Boot/production.php index 73c7c60afbc7..1822cf583f82 100644 --- a/app/Config/Boot/production.php +++ b/app/Config/Boot/production.php @@ -9,8 +9,10 @@ | | If you set 'display_errors' to '1', CI4's detailed error report will show. */ +error_reporting(E_ALL & ~E_DEPRECATED); +// If you want to suppress more types of errors. +// error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); ini_set('display_errors', '0'); -error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); /* |-------------------------------------------------------------------------- diff --git a/app/Config/Cache.php b/app/Config/Cache.php index b29c13a9ea71..3fbade6840cc 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -46,25 +46,6 @@ class Cache extends BaseConfig */ public string $storePath = WRITEPATH . 'cache/'; - /** - * -------------------------------------------------------------------------- - * Cache Include Query String - * -------------------------------------------------------------------------- - * - * Whether to take the URL query string into consideration when generating - * output cache files. Valid options are: - * - * false = Disabled - * true = Enabled, take all query parameters into account. - * Please be aware that this may result in numerous cache - * files generated for the same page over and over again. - * ['q'] = Enabled, but only take into account the specified list - * of query parameters. - * - * @var bool|list - */ - public $cacheQueryString = false; - /** * -------------------------------------------------------------------------- * Key Prefix @@ -168,4 +149,23 @@ class Cache extends BaseConfig 'redis' => RedisHandler::class, 'wincache' => WincacheHandler::class, ]; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Include Query String + * -------------------------------------------------------------------------- + * + * Whether to take the URL query string into consideration when generating + * output cache files. Valid options are: + * + * false = Disabled + * true = Enabled, take all query parameters into account. + * Please be aware that this may result in numerous cache + * files generated for the same page over and over again. + * ['q'] = Enabled, but only take into account the specified list + * of query parameters. + * + * @var bool|list + */ + public $cacheQueryString = false; } diff --git a/app/Config/Cors.php b/app/Config/Cors.php new file mode 100644 index 000000000000..2b4edf6bf60f --- /dev/null +++ b/app/Config/Cors.php @@ -0,0 +1,105 @@ +, + * allowedOriginsPatterns: list, + * supportsCredentials: bool, + * allowedHeaders: list, + * exposedHeaders: list, + * allowedMethods: list, + * maxAge: int, + * } + */ + public array $default = [ + /** + * Origins for the `Access-Control-Allow-Origin` header. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + * + * E.g.: + * - ['http://localhost:8080'] + * - ['https://www.example.com'] + */ + 'allowedOrigins' => [], + + /** + * Origin regex patterns for the `Access-Control-Allow-Origin` header. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + * + * NOTE: A pattern specified here is part of a regular expression. It will + * be actually `#\A\z#`. + * + * E.g.: + * - ['https://\w+\.example\.com'] + */ + 'allowedOriginsPatterns' => [], + + /** + * Weather to send the `Access-Control-Allow-Credentials` header. + * + * The Access-Control-Allow-Credentials response header tells browsers whether + * the server allows cross-origin HTTP requests to include credentials. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + */ + 'supportsCredentials' => false, + + /** + * Set headers to allow. + * + * The Access-Control-Allow-Headers response header is used in response to + * a preflight request which includes the Access-Control-Request-Headers to + * indicate which HTTP headers can be used during the actual request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + */ + 'allowedHeaders' => [], + + /** + * Set headers to expose. + * + * The Access-Control-Expose-Headers response header allows a server to + * indicate which response headers should be made available to scripts running + * in the browser, in response to a cross-origin request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + */ + 'exposedHeaders' => [], + + /** + * Set methods to allow. + * + * The Access-Control-Allow-Methods response header specifies one or more + * methods allowed when accessing a resource in response to a preflight + * request. + * + * E.g.: + * - ['GET', 'POST', 'PUT', 'DELETE'] + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + */ + 'allowedMethods' => [], + + /** + * Set how many seconds the results of a preflight request can be cached. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + */ + 'maxAge' => 7200, + ]; +} diff --git a/app/Config/Database.php b/app/Config/Database.php index 8c82360240b6..7a1fd21e8d10 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -10,14 +10,12 @@ class Database extends Config { /** - * The directory that holds the Migrations - * and Seeds directories. + * The directory that holds the Migrations and Seeds directories. */ public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR; /** - * Lets you choose which connection group to - * use if no other is specified. + * Lets you choose which connection group to use if no other is specified. */ public string $defaultGroup = 'default'; @@ -36,8 +34,8 @@ class Database extends Config 'DBPrefix' => '', 'pConnect' => false, 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'charset' => 'utf8mb4', + 'DBCollat' => 'utf8mb4_general_ci', 'swapPre' => '', 'encrypt' => false, 'compress' => false, @@ -45,11 +43,120 @@ class Database extends Config 'failover' => [], 'port' => 3306, 'numberNative' => false, + 'dateFormat' => [ + 'date' => 'Y-m-d', + 'datetime' => 'Y-m-d H:i:s', + 'time' => 'H:i:s', + ], ]; + // /** + // * Sample database connection for SQLite3. + // * + // * @var array + // */ + // public array $default = [ + // 'database' => 'database.db', + // 'DBDriver' => 'SQLite3', + // 'DBPrefix' => '', + // 'DBDebug' => true, + // 'swapPre' => '', + // 'failover' => [], + // 'foreignKeys' => true, + // 'busyTimeout' => 1000, + // 'dateFormat' => [ + // 'date' => 'Y-m-d', + // 'datetime' => 'Y-m-d H:i:s', + // 'time' => 'H:i:s', + // ], + // ]; + + // /** + // * Sample database connection for Postgre. + // * + // * @var array + // */ + // public array $default = [ + // 'DSN' => '', + // 'hostname' => 'localhost', + // 'username' => 'root', + // 'password' => 'root', + // 'database' => 'ci4', + // 'schema' => 'public', + // 'DBDriver' => 'Postgre', + // 'DBPrefix' => '', + // 'pConnect' => false, + // 'DBDebug' => true, + // 'charset' => 'utf8', + // 'swapPre' => '', + // 'failover' => [], + // 'port' => 5432, + // 'dateFormat' => [ + // 'date' => 'Y-m-d', + // 'datetime' => 'Y-m-d H:i:s', + // 'time' => 'H:i:s', + // ], + // ]; + + // /** + // * Sample database connection for SQLSRV. + // * + // * @var array + // */ + // public array $default = [ + // 'DSN' => '', + // 'hostname' => 'localhost', + // 'username' => 'root', + // 'password' => 'root', + // 'database' => 'ci4', + // 'schema' => 'dbo', + // 'DBDriver' => 'SQLSRV', + // 'DBPrefix' => '', + // 'pConnect' => false, + // 'DBDebug' => true, + // 'charset' => 'utf8', + // 'swapPre' => '', + // 'encrypt' => false, + // 'failover' => [], + // 'port' => 1433, + // 'dateFormat' => [ + // 'date' => 'Y-m-d', + // 'datetime' => 'Y-m-d H:i:s', + // 'time' => 'H:i:s', + // ], + // ]; + + // /** + // * Sample database connection for OCI8. + // * + // * You may need the following environment variables: + // * NLS_LANG = 'AMERICAN_AMERICA.UTF8' + // * NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS' + // * NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS' + // * NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS' + // * + // * @var array + // */ + // public array $default = [ + // 'DSN' => 'localhost:1521/XEPDB1', + // 'username' => 'root', + // 'password' => 'root', + // 'DBDriver' => 'OCI8', + // 'DBPrefix' => '', + // 'pConnect' => false, + // 'DBDebug' => true, + // 'charset' => 'AL32UTF8', + // 'swapPre' => '', + // 'failover' => [], + // 'dateFormat' => [ + // 'date' => 'Y-m-d', + // 'datetime' => 'Y-m-d H:i:s', + // 'time' => 'H:i:s', + // ], + // ]; + /** - * This database connection is used when - * running PHPUnit database tests. + * This database connection is used when running PHPUnit database tests. * * @var array */ @@ -64,7 +171,7 @@ class Database extends Config 'pConnect' => false, 'DBDebug' => true, 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'DBCollat' => '', 'swapPre' => '', 'encrypt' => false, 'compress' => false, @@ -73,6 +180,11 @@ class Database extends Config 'port' => 3306, 'foreignKeys' => true, 'busyTimeout' => 1000, + 'dateFormat' => [ + 'date' => 'Y-m-d', + 'datetime' => 'Y-m-d H:i:s', + 'time' => 'H:i:s', + ], ]; public function __construct() diff --git a/app/Config/Feature.php b/app/Config/Feature.php index 0bc45c6f46c0..efd4a0b20ae8 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -10,21 +10,20 @@ class Feature extends BaseConfig { /** - * Enable multiple filters for a route or not. - * - * If you enable this: - * - CodeIgniter\CodeIgniter::handleRequest() uses: - * - CodeIgniter\Filters\Filters::enableFilters(), instead of enableFilter() - * - CodeIgniter\CodeIgniter::tryToRouteIt() uses: - * - CodeIgniter\Router\Router::getFilters(), instead of getFilter() - * - CodeIgniter\Router\Router::handle() uses: - * - property $filtersInfo, instead of $filterInfo - * - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute() + * Use improved new auto routing instead of the default legacy version. */ - public bool $multipleFilters = false; + public bool $autoRoutesImproved = false; /** - * Use improved new auto routing instead of the default legacy version. + * Use filter execution order in 4.4 or before. */ - public bool $autoRoutesImproved = false; + public bool $oldFilterOrder = false; + + /** + * The behavior of `limit(0)` in Query Builder. + * + * If true, `limit(0)` returns all records. (the behavior of 4.4.x or before in version 4.x.) + * If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.) + */ + public bool $limitZeroAsAll = true; } diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 57aaed2e9b7b..eb46a1d7cee8 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -2,21 +2,27 @@ namespace Config; -use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Config\Filters as BaseFilters; +use CodeIgniter\Filters\Cors; use CodeIgniter\Filters\CSRF; use CodeIgniter\Filters\DebugToolbar; +use CodeIgniter\Filters\ForceHTTPS; use CodeIgniter\Filters\Honeypot; use CodeIgniter\Filters\InvalidChars; +use CodeIgniter\Filters\PageCache; +use CodeIgniter\Filters\PerformanceMetrics; use CodeIgniter\Filters\SecureHeaders; -class Filters extends BaseConfig +class Filters extends BaseFilters { /** * Configures aliases for Filter classes to * make reading things nicer and simpler. * - * @var array> [filter_name => classname] - * or [filter_name => [classname1, classname2, ...]] + * @var array> + * + * [filter_name => classname] + * or [filter_name => [classname1, classname2, ...]] */ public array $aliases = [ 'csrf' => CSRF::class, @@ -24,6 +30,35 @@ class Filters extends BaseConfig 'honeypot' => Honeypot::class, 'invalidchars' => InvalidChars::class, 'secureheaders' => SecureHeaders::class, + 'cors' => Cors::class, + 'forcehttps' => ForceHTTPS::class, + 'pagecache' => PageCache::class, + 'performance' => PerformanceMetrics::class, + ]; + + /** + * List of special required filters. + * + * The filters listed here are special. They are applied before and after + * other kinds of filters, and always applied even if a route does not exist. + * + * Filters set by default provide framework functionality. If removed, + * those functions will no longer work. + * + * @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters + * + * @var array{before: list, after: list} + */ + public array $required = [ + 'before' => [ + 'forcehttps', // Force Global Secure Requests + 'pagecache', // Web Page Caching + ], + 'after' => [ + 'pagecache', // Web Page Caching + 'performance', // Performance Metrics + 'toolbar', // Debug Toolbar + ], ]; /** @@ -39,7 +74,6 @@ class Filters extends BaseConfig // 'invalidchars', ], 'after' => [ - 'toolbar', // 'honeypot', // 'secureheaders', ], @@ -50,7 +84,7 @@ class Filters extends BaseConfig * particular HTTP method (GET, POST, etc.). * * Example: - * 'post' => ['foo', 'bar'] + * 'POST' => ['foo', 'bar'] * * If you use this, you should disable auto-routing because auto-routing * permits any HTTP method to access a controller. Accessing the controller diff --git a/app/Config/Generators.php b/app/Config/Generators.php index 6566a31e851e..cc92c7aa432f 100644 --- a/app/Config/Generators.php +++ b/app/Config/Generators.php @@ -23,11 +23,13 @@ class Generators extends BaseConfig * * YOU HAVE BEEN WARNED! * - * @var array + * @var array|string> */ public array $views = [ - 'make:cell' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php', - 'make:cell_view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php', + 'make:cell' => [ + 'class' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php', + 'view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php', + ], 'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', 'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', 'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', diff --git a/app/Config/Kint.php b/app/Config/Kint.php index 117e66d87c45..d07078270145 100644 --- a/app/Config/Kint.php +++ b/app/Config/Kint.php @@ -2,7 +2,6 @@ namespace Config; -use CodeIgniter\Config\BaseConfig; use Kint\Parser\ConstructablePluginInterface; use Kint\Renderer\AbstractRenderer; use Kint\Renderer\Rich\TabPluginInterface; @@ -18,7 +17,7 @@ * * @see https://kint-php.github.io/kint/ for details on these settings. */ -class Kint extends BaseConfig +class Kint { /* |-------------------------------------------------------------------------- diff --git a/app/Config/Optimize.php b/app/Config/Optimize.php new file mode 100644 index 000000000000..6fb441fd2288 --- /dev/null +++ b/app/Config/Optimize.php @@ -0,0 +1,32 @@ + */ public array $moduleRoutes = []; + + /** + * For Auto Routing (Improved). + * Whether to translate dashes in URIs for controller/method to CamelCase. + * E.g., blog-controller -> BlogController + * + * If you enable this, $translateURIDashes is ignored. + * + * Default: false + */ + public bool $translateUriToCamelCase = false; } diff --git a/app/Config/Security.php b/app/Config/Security.php index 57be4ee41541..0858b9bbb257 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -79,8 +79,10 @@ class Security extends BaseConfig * -------------------------------------------------------------------------- * * Redirect to previous page with error on failure. + * + * @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure */ - public bool $redirect = false; + public bool $redirect = (ENVIRONMENT === 'production'); /** * -------------------------------------------------------------------------- diff --git a/app/Config/Session.php b/app/Config/Session.php index e077df64bd46..6944710f705b 100644 --- a/app/Config/Session.php +++ b/app/Config/Session.php @@ -99,4 +99,29 @@ class Session extends BaseConfig * DB Group for the database session. */ public ?string $DBGroup = null; + + /** + * -------------------------------------------------------------------------- + * Lock Retry Interval (microseconds) + * -------------------------------------------------------------------------- + * + * This is used for RedisHandler. + * + * Time (microseconds) to wait if lock cannot be acquired. + * The default is 100,000 microseconds (= 0.1 seconds). + */ + public int $lockRetryInterval = 100_000; + + /** + * -------------------------------------------------------------------------- + * Lock Max Retries + * -------------------------------------------------------------------------- + * + * This is used for RedisHandler. + * + * Maximum number of lock acquisition attempts. + * The default is 300 times. That is lock timeout is about 30 (0.1 * 300) + * seconds. + */ + public int $lockMaxRetries = 300; } diff --git a/app/Views/errors/cli/error_exception.php b/app/Views/errors/cli/error_exception.php index 98d83b0ed3a6..9f47d25141d2 100644 --- a/app/Views/errors/cli/error_exception.php +++ b/app/Views/errors/cli/error_exception.php @@ -3,7 +3,7 @@ use CodeIgniter\CLI\CLI; // The main Exception -CLI::write('[' . get_class($exception) . ']', 'light_gray', 'red'); +CLI::write('[' . $exception::class . ']', 'light_gray', 'red'); CLI::write($message); CLI::write('at ' . CLI::color(clean_path($exception->getFile()) . ':' . $exception->getLine(), 'green')); CLI::newLine(); @@ -14,7 +14,7 @@ $last = $prevException; CLI::write(' Caused by:'); - CLI::write(' [' . get_class($prevException) . ']', 'red'); + CLI::write(' [' . $prevException::class . ']', 'red'); CLI::write(' ' . $prevException->getMessage()); CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green')); CLI::newLine(); @@ -50,20 +50,11 @@ $function .= $padClass . $error['function']; } - $args = implode(', ', array_map(static function ($value) { - switch (true) { - case is_object($value): - return 'Object(' . get_class($value) . ')'; - - case is_array($value): - return count($value) ? '[...]' : '[]'; - - case $value === null: - return 'null'; // return the lowercased version - - default: - return var_export($value, true); - } + $args = implode(', ', array_map(static fn ($value) => match (true) { + is_object($value) => 'Object(' . $value::class . ')', + is_array($value) => count($value) ? '[...]' : '[]', + $value === null => 'null', // return the lowercased version + default => var_export($value, true), }, array_values($error['args'] ?? []))); $function .= '(' . $args . ')'; diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php index 047c2f4cd987..44d749892473 100644 --- a/app/Views/errors/html/error_exception.php +++ b/app/Views/errors/html/error_exception.php @@ -1,4 +1,5 @@ Caused by: - getCode() ? ' #' . $prevException->getCode() : '') ?> + getCode() ? ' #' . $prevException->getCode() : '') ?> getMessage())) ?> - getMessage())) ?>" + getMessage())) ?>" rel="noreferrer" target="_blank">search → getFile()) . ':' . $prevException->getLine()) ?> @@ -120,7 +121,7 @@ getParameters(); } @@ -234,7 +235,7 @@ HTTP Method - getMethod())) ?> + getMethod()) ?> IP Address @@ -318,10 +319,20 @@ - + $value) : ?> - getName(), 'html') ?> - getValueLine(), 'html') ?> + + + getValueLine(), 'html'); + } else { + foreach ($value as $i => $header) { + echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html'); + } + } + ?> + @@ -345,8 +356,6 @@ headers(); ?> - -

Headers

@@ -357,10 +366,20 @@ - + $value) : ?> - + diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php index 919629fc77ef..c18eca3c9ed3 100644 --- a/app/Views/welcome_message.php +++ b/app/Views/welcome_message.php @@ -297,7 +297,7 @@
-

Page rendered in {elapsed_time} seconds

+

Page rendered in {elapsed_time} seconds using {memory_usage} MB of memory.

Environment:

diff --git a/composer.json b/composer.json index b5baf427017a..9c17f59ded32 100644 --- a/composer.json +++ b/composer.json @@ -10,12 +10,11 @@ "slack": "https://codeigniterchat.slack.com" }, "require": { - "php": "^7.4 || ^8.0", + "php": "^8.1", "ext-intl": "*", - "ext-json": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.9", - "psr/log": "^1.1" + "laminas/laminas-escaper": "^2.13", + "psr/log": "^3.0" }, "require-dev": { "codeigniter/coding-standard": "^1.7", @@ -26,13 +25,12 @@ "kint-php/kint": "^5.0.4", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", - "nexusphp/tachycardia": "^1.0", - "php-coveralls/php-coveralls": "^2.5", + "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.3", "phpstan/phpstan": "^1.10.2", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpcov": "^8.2", - "phpunit/phpunit": "^9.1", + "phpunit/phpcov": "^9.0.2", + "phpunit/phpunit": "^10.5.16", "predis/predis": "^1.1 || ^2.0", "rector/rector": "1.0.4", "vimeo/psalm": "^5.0" @@ -91,7 +89,8 @@ }, "scripts": { "post-update-cmd": [ - "CodeIgniter\\ComposerScripts::postUpdate" + "CodeIgniter\\ComposerScripts::postUpdate", + "composer update --working-dir=tools/phpmetrics" ], "analyze": [ "Composer\\Config::disableProcessTimeout", @@ -110,6 +109,7 @@ "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", "php-cs-fixer fix --ansi --verbose --diff" ], + "metrics": "tools/phpmetrics/vendor/bin/phpmetrics --config=phpmetrics.json", "sa": "@analyze", "style": "@cs-fix", "test": "phpunit" @@ -118,6 +118,7 @@ "analyze": "Run static analysis", "cs": "Check the coding style", "cs-fix": "Fix the coding style", + "metrics": "Run PhpMetrics", "test": "Run unit tests" } } diff --git a/contributing/internals.md b/contributing/internals.md index e2d387028fe1..b49c9832d390 100644 --- a/contributing/internals.md +++ b/contributing/internals.md @@ -29,10 +29,9 @@ PHP7 provides [Type declarations](https://www.php.net/manual/en/language.types.d for method parameters and return types. Use it where possible. Return type declaration is not always practical, but do try to make it work. -At this time, shipped CI4 production code does not use -[Strict typing](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict), -and will not be any time soon. However, in the development phase, -there are internal classes (in `utils/`) that are strictly typed. +At this time, shipped CI4 production code does use +[Strict typing](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict) +as much as possible. ## Abstractions diff --git a/contributing/pull_request.md b/contributing/pull_request.md index 38f32d40c1be..4b5b06406124 100644 --- a/contributing/pull_request.md +++ b/contributing/pull_request.md @@ -154,7 +154,7 @@ See [Contribution CSS](./css.md). ### Compatibility -CodeIgniter4 requires [PHP 7.4](https://php.net/releases/7_4_0.php). +CodeIgniter4 requires [PHP 8.1](https://php.net/releases/8_1_0.php). ### Backwards Compatibility diff --git a/deptrac.yaml b/deptrac.yaml index 271378bfde55..178de9a74fd7 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -35,6 +35,14 @@ parameters: collectors: - type: className regex: ^Codeigniter\\Database\\.* + - name: DataCaster + collectors: + - type: className + regex: ^Codeigniter\\DataCaster\\.* + - name: DataConverter + collectors: + - type: className + regex: ^Codeigniter\\DataConverter\\.* - name: Email collectors: - type: className @@ -166,10 +174,17 @@ parameters: - Entity - Events - I18n + DataCaster: + - I18n + - URI + - Database + DataConverter: + - DataCaster Email: - I18n - Events Entity: + - DataCaster - I18n Files: - I18n @@ -189,6 +204,7 @@ parameters: - I18n Model: - Database + - DataConverter - Entity - I18n - Pager @@ -219,18 +235,29 @@ parameters: - I18n Validation: - HTTP + - Database View: - Cache skip_violations: # Individual class exemptions CodeIgniter\Cache\ResponseCache: - CodeIgniter\HTTP\CLIRequest + - CodeIgniter\HTTP\Header - CodeIgniter\HTTP\IncomingRequest - CodeIgniter\HTTP\ResponseInterface + CodeIgniter\DataCaster\DataCaster: + - CodeIgniter\Entity\Cast\CastInterface + - CodeIgniter\Entity\Exceptions\CastException + CodeIgniter\DataCaster\Exceptions\CastException: + - CodeIgniter\Entity\Exceptions\CastException + CodeIgniter\DataConverter\DataConverter: + - CodeIgniter\Entity\Entity CodeIgniter\Entity\Cast\URICast: - CodeIgniter\HTTP\URI CodeIgniter\Log\Handlers\ChromeLoggerHandler: - CodeIgniter\HTTP\ResponseInterface + CodeIgniter\Security\CheckPhpIni: + - CodeIgniter\View\Table CodeIgniter\View\Table: - CodeIgniter\Database\BaseResult CodeIgniter\View\Plugins: diff --git a/env b/env index e60354b3c4f8..f359ec20bff2 100644 --- a/env +++ b/env @@ -38,106 +38,32 @@ # database.default.DBPrefix = # database.default.port = 3306 +# If you use MySQLi as tests, first update the values of Config\Database::$tests. # database.tests.hostname = localhost # database.tests.database = ci4_test # database.tests.username = root # database.tests.password = root # database.tests.DBDriver = MySQLi # database.tests.DBPrefix = +# database.tests.charset = utf8mb4 +# database.tests.DBCollat = utf8mb4_general_ci # database.tests.port = 3306 -#-------------------------------------------------------------------- -# CONTENT SECURITY POLICY -#-------------------------------------------------------------------- - -# contentsecuritypolicy.reportOnly = false -# contentsecuritypolicy.defaultSrc = 'none' -# contentsecuritypolicy.scriptSrc = 'self' -# contentsecuritypolicy.styleSrc = 'self' -# contentsecuritypolicy.imageSrc = 'self' -# contentsecuritypolicy.baseURI = null -# contentsecuritypolicy.childSrc = null -# contentsecuritypolicy.connectSrc = 'self' -# contentsecuritypolicy.fontSrc = null -# contentsecuritypolicy.formAction = null -# contentsecuritypolicy.frameAncestors = null -# contentsecuritypolicy.frameSrc = null -# contentsecuritypolicy.mediaSrc = null -# contentsecuritypolicy.objectSrc = null -# contentsecuritypolicy.pluginTypes = null -# contentsecuritypolicy.reportURI = null -# contentsecuritypolicy.sandbox = false -# contentsecuritypolicy.upgradeInsecureRequests = false -# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}' -# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}' -# contentsecuritypolicy.autoNonce = true - -#-------------------------------------------------------------------- -# COOKIE -#-------------------------------------------------------------------- - -# cookie.prefix = '' -# cookie.expires = 0 -# cookie.path = '/' -# cookie.domain = '' -# cookie.secure = false -# cookie.httponly = false -# cookie.samesite = 'Lax' -# cookie.raw = false - #-------------------------------------------------------------------- # ENCRYPTION #-------------------------------------------------------------------- # encryption.key = -# encryption.driver = OpenSSL -# encryption.blockSize = 16 -# encryption.digest = SHA512 - -#-------------------------------------------------------------------- -# HONEYPOT -#-------------------------------------------------------------------- - -# honeypot.hidden = 'true' -# honeypot.label = 'Fill This Field' -# honeypot.name = 'honeypot' -# honeypot.template = '' -# honeypot.container = '
{template}
' - -#-------------------------------------------------------------------- -# SECURITY -#-------------------------------------------------------------------- - -# security.csrfProtection = 'cookie' -# security.tokenRandomize = false -# security.tokenName = 'csrf_token_name' -# security.headerName = 'X-CSRF-TOKEN' -# security.cookieName = 'csrf_cookie_name' -# security.expires = 7200 -# security.regenerate = true -# security.redirect = false -# security.samesite = 'Lax' #-------------------------------------------------------------------- # SESSION #-------------------------------------------------------------------- # session.driver = 'CodeIgniter\Session\Handlers\FileHandler' -# session.cookieName = 'ci_session' -# session.expiration = 7200 # session.savePath = null -# session.matchIP = false -# session.timeToUpdate = 300 -# session.regenerateDestroy = false #-------------------------------------------------------------------- # LOGGER #-------------------------------------------------------------------- # logger.threshold = 4 - -#-------------------------------------------------------------------- -# CURLRequest -#-------------------------------------------------------------------- - -# curlrequest.shareOptions = false diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index 616f6aeae98d..b93122ac193f 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -5,12 +5,12 @@ xmlns="https://www.phpdoc.org" xsi:noNamespaceSchemaLocation="https://docs.phpdoc.org/latest/phpdoc.xsd" > - CodeIgniter v4.4 API + CodeIgniter v4.5 API api/build/ api/cache/ - + system diff --git a/phpmetrics.json b/phpmetrics.json new file mode 100644 index 000000000000..3ff72a669193 --- /dev/null +++ b/phpmetrics.json @@ -0,0 +1,7 @@ +{ + "includes": ["system"], + "excludes": ["ThirdParty"], + "report": { + "html": "build/phpmetrics/" + } +} diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 8dd1905e6b96..ffe358b48943 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -2,29 +2,29 @@ $ignoreErrors = []; $ignoreErrors[] = [ - 'message' => '#^Call to function method_exists\\(\\) with \'Composer\\\\\\\\InstalledVersions\' and \'getAllRawData\' will always evaluate to true\\.$#', + 'message' => '#^PHPDoc type array\\\\> of property Config\\\\Filters\\:\\:\\$methods is not the same as PHPDoc type array of overridden property CodeIgniter\\\\Config\\\\Filters\\:\\:\\$methods\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', + 'path' => __DIR__ . '/app/Config/Filters.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\Autoloader\\:\\:getNamespace\\(\\) return type has no value type specified in iterable type array\\.$#', + 'message' => '#^PHPDoc type array\\\\>\\> of property Config\\\\Filters\\:\\:\\$filters is not the same as PHPDoc type array of overridden property CodeIgniter\\\\Config\\\\Filters\\:\\:\\$filters\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', + 'path' => __DIR__ . '/app/Config/Filters.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\Autoloader\\:\\:loadComposerNamespaces\\(\\) has parameter \\$composerPackages with no value type specified in iterable type array\\.$#', + 'message' => '#^Call to function method_exists\\(\\) with \'Composer\\\\\\\\InstalledVersions\' and \'getAllRawData\' will always evaluate to true\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property Config\\\\Autoload\\:\\:\\$helpers \\(array\\\\) in isset\\(\\) is not nullable\\.$#', + 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\Autoloader\\:\\:loadComposerNamespaces\\(\\) has parameter \\$composerPackages with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\FileLocator\\:\\:search\\(\\) return type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property Config\\\\Autoload\\:\\:\\$helpers \\(array\\\\) in isset\\(\\) is not nullable\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Autoloader/FileLocator.php', + 'path' => __DIR__ . '/system/Autoloader/Autoloader.php', ]; $ignoreErrors[] = [ 'message' => '#^Only booleans are allowed in a ternary operator condition, int\\|string given\\.$#', @@ -36,6 +36,21 @@ 'count' => 6, 'path' => __DIR__ . '/system/Autoloader/FileLocator.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\FileLocatorCached\\:\\:search\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Autoloader/FileLocatorCached.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Autoloader\\\\FileLocatorCached\\:\\:\\$cache type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Autoloader/FileLocatorCached.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\:\\:search\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Autoloader/FileLocatorInterface.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:__call\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -61,6 +76,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/BaseModel.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:convertToReturnType\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/BaseModel.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:delete\\(\\) has parameter \\$id with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -126,21 +146,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/BaseModel.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:fillPlaceholders\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/BaseModel.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:fillPlaceholders\\(\\) has parameter \\$rules with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/BaseModel.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:fillPlaceholders\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/BaseModel.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:find\\(\\) has parameter \\$id with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -181,16 +186,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/BaseModel.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:idValue\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/BaseModel.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:idValue\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/BaseModel.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\BaseModel\\:\\:paginate\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -321,11 +316,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/CLI/CLI.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\CLI\\\\CLI\\:\\:prompt\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/CLI/CLI.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\CLI\\\\CLI\\:\\:prompt\\(\\) has parameter \\$validation with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -371,11 +361,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/CLI/CLI.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, array given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/CLI/CLI.php', -]; $ignoreErrors[] = [ 'message' => '#^Only booleans are allowed in &&, array\\ given on the right side\\.$#', 'count' => 1, @@ -751,31 +736,16 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Encryption/GenerateKey.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CellGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CellGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CellGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CellGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CellGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -801,56 +771,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\CellGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/CellGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CommandGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CommandGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CommandGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CommandGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\CommandGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -876,56 +811,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\CommandGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/CommandGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ConfigGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ConfigGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ConfigGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ConfigGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ConfigGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -951,56 +851,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\ConfigGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ConfigGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ControllerGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ControllerGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ControllerGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ControllerGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ControllerGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1026,76 +891,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in an elseif condition, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in an if condition, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in \\|\\|, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in \\|\\|, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\ControllerGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ControllerGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\EntityGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\EntityGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\EntityGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\EntityGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\EntityGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1121,56 +931,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\EntityGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/EntityGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\FilterGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\FilterGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\FilterGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\FilterGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\FilterGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1196,56 +971,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\FilterGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/FilterGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\MigrationGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\MigrationGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\MigrationGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\MigrationGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\MigrationGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1271,61 +1011,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in an if condition, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\MigrationGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/MigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ModelGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ModelGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ModelGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ModelGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ModelGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1351,61 +1051,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in an if condition, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\ModelGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ModelGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1429,218 +1089,103 @@ $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:renderTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in an elseif condition, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in an if condition, mixed given\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:parseTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:parseTemplate\\(\\) has parameter \\$replace with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:parseTemplate\\(\\) has parameter \\$search with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:renderTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', + 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\ScaffoldGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/ScaffoldGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', + 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:parseTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:parseTemplate\\(\\) has parameter \\$replace with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:parseTemplate\\(\\) has parameter \\$search with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:renderTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', + 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\SeederGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/SeederGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:parseTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:parseTemplate\\(\\) has parameter \\$replace with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:parseTemplate\\(\\) has parameter \\$search with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:renderTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:parseTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:parseTemplate\\(\\) has parameter \\$replace with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:parseTemplate\\(\\) has parameter \\$search with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:renderTemplate\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\SessionMigrationGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\TestGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/SessionMigrationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', + 'path' => __DIR__ . '/system/Commands/Generators/TestGenerator.php', ]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ValidationGenerator\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ValidationGenerator\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ValidationGenerator\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ValidationGenerator\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Generators\\\\ValidationGenerator\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1666,31 +1211,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Commands\\\\Generators\\\\ValidationGenerator\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Commands/Generators/ValidationGenerator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\ListCommands\\:\\:listFull\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -1716,11 +1241,51 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Server/Serve.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinder\\:\\:arrayToTableRows\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Translation/LocalizationFinder.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinder\\:\\:arrayToTableRows\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Translation/LocalizationFinder.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinder\\:\\:buildMultiArray\\(\\) has parameter \\$fromKeys with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Translation/LocalizationFinder.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinder\\:\\:buildMultiArray\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Translation/LocalizationFinder.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinder\\:\\:findTranslationsInFile\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Translation/LocalizationFinder.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinder\\:\\:templateFile\\(\\) has parameter \\$language with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Translation/LocalizationFinder.php', +]; $ignoreErrors[] = [ 'message' => '#^Accessing offset \'CI_ENVIRONMENT\' directly on \\$_SERVER is discouraged\\.$#', 'count' => 3, 'path' => __DIR__ . '/system/Commands/Utilities/Environment.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\FilterCheck\\:\\:addRequiredFilters\\(\\) has parameter \\$filters with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Utilities/FilterCheck.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\FilterCheck\\:\\:addRequiredFilters\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Commands/Utilities/FilterCheck.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Namespaces\\:\\:outputAllNamespaces\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1776,11 +1341,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\AutoRouterImproved\\\\AutoRouteCollector\\:\\:__construct\\(\\) has parameter \\$protectedControllers with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\AutoRouterImproved\\\\AutoRouteCollector\\:\\:addFilters\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -1796,16 +1356,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\AutoRouterImproved\\\\AutoRouteCollector\\:\\:\\$httpMethods type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\AutoRouterImproved\\\\ControllerMethodReader\\:\\:__construct\\(\\) has parameter \\$httpMethods with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\AutoRouterImproved\\\\ControllerMethodReader\\:\\:getParameters\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -1993,7 +1543,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/system/Config/BaseService.php', ]; $ignoreErrors[] = [ @@ -2001,31 +1551,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Config/BaseService.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Config\\\\BaseService\\:\\:discoverServices\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Config/BaseService.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Config\\\\BaseService\\:\\:getSharedInstance\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Config/BaseService.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, array given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Config/BaseService.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Config\\\\BaseService\\:\\:\\$instances type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Config/BaseService.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Config\\\\BaseService\\:\\:\\$mocks type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Config/BaseService.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Config\\\\BaseService\\:\\:\\$services type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -2106,11 +1636,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Config/Factories.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, string given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Config/Factories.php', -]; $ignoreErrors[] = [ 'message' => '#^Only booleans are allowed in a negated boolean, array given\\.$#', 'count' => 1, @@ -2131,6 +1656,16 @@ 'count' => 1, 'path' => __DIR__ . '/system/Config/Factory.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Config\\\\Filters\\:\\:\\$filters type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Config/Filters.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Config\\\\Filters\\:\\:\\$methods type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Config/Filters.php', +]; $ignoreErrors[] = [ 'message' => '#^Accessing offset \'SERVER_PROTOCOL\' directly on \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -2236,6 +1771,31 @@ 'count' => 1, 'path' => __DIR__ . '/system/Cookie/CookieStore.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\DataCaster\\\\Cast\\\\ArrayCast\\:\\:get\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/DataCaster/Cast/ArrayCast.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\DataCaster\\\\Cast\\\\CSVCast\\:\\:get\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/DataCaster/Cast/CSVCast.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\DataCaster\\\\Cast\\\\JsonCast\\:\\:get\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/DataCaster/Cast/JsonCast.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverter\\:\\:fromDataSource\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/DataConverter/DataConverter.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverter\\:\\:toDataSource\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/DataConverter/DataConverter.php', +]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 30, @@ -3433,7 +2993,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Only booleans are allowed in a ternary operator condition, int\\<0, max\\> given\\.$#', - 'count' => 4, + 'count' => 3, 'path' => __DIR__ . '/system/Database/MigrationRunner.php', ]; $ignoreErrors[] = [ @@ -3451,16 +3011,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Database/MigrationRunner.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\ModelFactory\\:\\:injectMock\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/ModelFactory.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\ModelFactory\\:\\:reset\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/ModelFactory.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Database\\\\MySQLi\\\\Builder\\:\\:\\$supportedIgnoreStatements type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -3498,7 +3048,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 6, + 'count' => 4, 'path' => __DIR__ . '/system/Database/MySQLi/Forge.php', ]; $ignoreErrors[] = [ @@ -5418,7 +4968,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 3, + 'count' => 1, 'path' => __DIR__ . '/system/Entity/Entity.php', ]; $ignoreErrors[] = [ @@ -5548,11 +5098,6 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Filters/Filters.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Expression on left side of \\?\\? is not nullable\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; @@ -5581,6 +5126,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:getRequiredFilters\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/Filters.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:pathApplies\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -5591,6 +5141,21 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:runAfter\\(\\) has parameter \\$filterClasses with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/Filters.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:runBefore\\(\\) has parameter \\$filterClasses with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/Filters.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\Filters\\:\\:setToolbarToLast\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/Filters.php', +]; $ignoreErrors[] = [ 'message' => '#^Only booleans are allowed in a negated boolean, array\\\\>\\> given\\.$#', 'count' => 1, @@ -5606,6 +5171,16 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/Filters.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\ForceHTTPS\\:\\:after\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/ForceHTTPS.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\ForceHTTPS\\:\\:before\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/ForceHTTPS.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Filters\\\\InvalidChars\\:\\:checkControl\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -5626,6 +5201,26 @@ 'count' => 1, 'path' => __DIR__ . '/system/Filters/InvalidChars.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\PageCache\\:\\:after\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/PageCache.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\PageCache\\:\\:before\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/PageCache.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\PerformanceMetrics\\:\\:after\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/PerformanceMetrics.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Filters\\\\PerformanceMetrics\\:\\:before\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Filters/PerformanceMetrics.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Format\\\\FormatterInterface\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -6436,11 +6031,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/HTTP/Message.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\HTTP\\\\Message\\:\\:header\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/Message.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\HTTP\\\\Message\\:\\:setHeader\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -6461,11 +6051,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/HTTP/Message.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\HTTP\\\\MessageInterface\\:\\:header\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/MessageInterface.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\HTTP\\\\MessageInterface\\:\\:setHeader\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -6701,11 +6286,6 @@ 'count' => 2, 'path' => __DIR__ . '/system/HTTP/Response.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\HTTP\\\\Response\\:\\:\\$cookies type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/Response.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\HTTP\\\\Response\\:\\:\\$statusCodes type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -6827,89 +6407,139 @@ 'path' => __DIR__ . '/system/HTTP/URI.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$fragment \\(string\\) on left side of \\?\\? is not nullable\\.$#', + 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$fragment \\(string\\) on left side of \\?\\? is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/HTTP/URI.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$host \\(string\\) on left side of \\?\\? is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/HTTP/URI.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$path \\(string\\) on left side of \\?\\? is not nullable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/HTTP/URI.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$query type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/HTTP/URI.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$segments type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/HTTP/URI.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Strict comparison using \\!\\=\\= between mixed and null will always evaluate to true\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/HTTP/URI.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Accessing offset \'HTTP_REFERER\' directly on \\$_SERVER is discouraged\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/system/HTTP/UserAgent.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Accessing offset \'HTTP_USER_AGENT\' directly on \\$_SERVER is discouraged\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/system/HTTP/UserAgent.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/system/HTTP/UserAgent.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Only booleans are allowed in &&, array\\ given on the right side\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/system/HTTP/UserAgent.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arrayAttachIndexedValue\\(\\) has parameter \\$indexes with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/URI.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$host \\(string\\) on left side of \\?\\? is not nullable\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arrayAttachIndexedValue\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/URI.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$path \\(string\\) on left side of \\?\\? is not nullable\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arrayAttachIndexedValue\\(\\) has parameter \\$row with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/URI.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$query type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arrayAttachIndexedValue\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/URI.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\HTTP\\\\URI\\:\\:\\$segments type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arraySearchDot\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/URI.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Strict comparison using \\!\\=\\= between mixed and null will always evaluate to true\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arraySearchDot\\(\\) has parameter \\$indexes with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/HTTP/URI.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Accessing offset \'HTTP_REFERER\' directly on \\$_SERVER is discouraged\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/system/HTTP/UserAgent.php', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:arraySearchDot\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Accessing offset \'HTTP_USER_AGENT\' directly on \\$_SERVER is discouraged\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/HTTP/UserAgent.php', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:dotKeyExists\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/HTTP/UserAgent.php', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:dotSearch\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, array\\ given on the right side\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/system/HTTP/UserAgent.php', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:dotSearch\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_attach_indexed_value\\(\\) has parameter \\$indexes with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:groupBy\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_attach_indexed_value\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:groupBy\\(\\) has parameter \\$indexes with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_attach_indexed_value\\(\\) has parameter \\$row with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:groupBy\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_attach_indexed_value\\(\\) return type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:recursiveCount\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_search_dot\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:recursiveDiff\\(\\) has parameter \\$compareWith with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_search_dot\\(\\) has parameter \\$indexes with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:recursiveDiff\\(\\) has parameter \\$original with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Function _array_search_dot\\(\\) return type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelper\\:\\:recursiveDiff\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Helpers/array_helper.php', + 'path' => __DIR__ . '/system/Helpers/Array/ArrayHelper.php', ]; $ignoreErrors[] = [ 'message' => '#^Function array_deep_search\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#', @@ -7726,11 +7356,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Model.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Model\\:\\:classToArray\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Model.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Model\\:\\:doDelete\\(\\) has parameter \\$id with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -7766,16 +7391,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Model.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Model\\:\\:idValue\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Model.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Model\\:\\:idValue\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Model.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Model\\:\\:set\\(\\) has parameter \\$key with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -7931,11 +7546,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/RESTful/ResourceController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouter\\:\\:__construct\\(\\) has parameter \\$cliRoutes with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/AutoRouter.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouter\\:\\:getRoute\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -7977,7 +7587,7 @@ 'path' => __DIR__ . '/system/Router/AutoRouterImproved.php', ]; $ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, Config\\\\Routing given on the right side\\.$#', + 'message' => '#^Property CodeIgniter\\\\Router\\\\AutoRouterImproved\\:\\:\\$moduleRoutes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Router/AutoRouterImproved.php', ]; @@ -8196,11 +7806,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Router/RouteCollection.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Router\\\\RouteCollection\\:\\:\\$defaultHTTPMethods type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/RouteCollection.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Router\\\\RouteCollection\\:\\:\\$routeFiles type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -8256,11 +7861,6 @@ 'count' => 2, 'path' => __DIR__ . '/system/Router/Router.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getFilterForRoute\\(\\)\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Router/Router.php', -]; $ignoreErrors[] = [ 'message' => '#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getFiltersForRoute\\(\\)\\.$#', 'count' => 1, @@ -8288,7 +7888,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 3, + 'count' => 2, 'path' => __DIR__ . '/system/Router/Router.php', ]; $ignoreErrors[] = [ @@ -8316,6 +7916,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Router/Router.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\Router\\:\\:replaceBackReferences\\(\\) has parameter \\$matches with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Router/Router.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\Router\\:\\:scanControllers\\(\\) has parameter \\$segments with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -8366,6 +7971,41 @@ 'count' => 1, 'path' => __DIR__ . '/system/Router/RouterInterface.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:checkIni\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:outputForCli\\(\\) has parameter \\$output with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:outputForCli\\(\\) has parameter \\$tbody with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:outputForCli\\(\\) has parameter \\$thead with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:outputForWeb\\(\\) has parameter \\$output with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:outputForWeb\\(\\) has parameter \\$tbody with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIni\\:\\:outputForWeb\\(\\) has parameter \\$thead with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Security/CheckPhpIni.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Session\\\\Exceptions\\\\SessionException\\:\\:forEmptySavepath\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -8458,12 +8098,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/system/Session/Handlers/RedisHandler.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, string given on the right side\\.$#', - 'count' => 1, + 'count' => 2, 'path' => __DIR__ . '/system/Session/Handlers/RedisHandler.php', ]; $ignoreErrors[] = [ @@ -9011,96 +8646,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Test/Fabricator.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Assigning \'test\' directly on offset \'HTTPS\' of \\$_SERVER is discouraged\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Assigning string directly on offset \'REQUEST_METHOD\' of \\$_SERVER is discouraged\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:call\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:delete\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:get\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:options\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:patch\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:populateGlobals\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:post\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:put\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:setRequestBody\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:withHeaders\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:withRoutes\\(\\) has parameter \\$routes with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\FeatureTestCase\\:\\:withSession\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, CodeIgniter\\\\Router\\\\RouteCollection\\|null given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Test\\\\CIUnitTestCase\\:\\:\\$bodyFormat \\(string\\) in isset\\(\\) is not nullable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Test\\\\CIUnitTestCase\\:\\:\\$clean \\(bool\\) in isset\\(\\) is not nullable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Test\\\\CIUnitTestCase\\:\\:\\$session \\(array\\) on left side of \\?\\? is not nullable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/FeatureTestCase.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Test\\\\Interfaces\\\\FabricatorModel\\:\\:find\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -9191,11 +8736,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Test/Mock/MockConnection.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:\\$returnValues has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/Mock/MockConnection.php', -]; $ignoreErrors[] = [ 'message' => '#^Return type \\(array\\{code\\: int\\|string\\|null, message\\: string\\|null\\}\\) of method CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:error\\(\\) should be covariant with return type \\(array\\\\) of method CodeIgniter\\\\Database\\\\ConnectionInterface\\\\:\\:error\\(\\)$#', 'count' => 1, @@ -9256,6 +8796,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Test/Mock/MockFileLogger.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Test\\\\Mock\\\\MockInputOutput\\:\\:getOutputs\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Test/Mock/MockInputOutput.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Test\\\\Mock\\\\MockLanguage\\:\\:disableIntlSupport\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -9376,11 +8921,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Test/Mock/MockResult.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Test\\\\Mock\\\\MockSecurityConfig\\:\\:\\$excludeURIs has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/Mock/MockSecurityConfig.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Test\\\\Mock\\\\MockServices\\:\\:\\$classmap has no type specified\\.$#', 'count' => 1, @@ -9429,145 +8969,35 @@ $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Test\\\\Mock\\\\MockTable\\:\\:__call\\(\\) has parameter \\$params with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Test/Mock/MockTable.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:register\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:restore\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:setContent\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:stream_stat\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:__call\\(\\) should return mixed but return statement is missing\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertCookie\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertCookieExpired\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertCookieMissing\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertDontSee\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertDontSeeElement\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertHeader\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertHeaderMissing\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertJSONExact\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertJSONExact\\(\\) has parameter \\$test with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertJSONFragment\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertJSONFragment\\(\\) has parameter \\$fragment with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertNotOK\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertNotRedirect\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertOK\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertRedirect\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertRedirectTo\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertSee\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', + 'path' => __DIR__ . '/system/Test/Mock/MockTable.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertSeeElement\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:register\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', + 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertSeeInField\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:restore\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', + 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertSeeLink\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:setContent\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', + 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertSessionHas\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Test\\\\PhpStreamWrapper\\:\\:stream_stat\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/system/Test/TestResponse.php', + 'path' => __DIR__ . '/system/Test/PhpStreamWrapper.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertSessionMissing\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertJSONExact\\(\\) has parameter \\$test with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Test/TestResponse.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertStatus\\(\\) has no return type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\Test\\\\TestResponse\\:\\:assertJSONFragment\\(\\) has parameter \\$fragment with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/Test/TestResponse.php', ]; @@ -9631,6 +9061,16 @@ 'count' => 1, 'path' => __DIR__ . '/system/Validation/Rules.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\Rules\\:\\:field_exists\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Validation/Rules.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\Rules\\:\\:field_exists\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Validation/Rules.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\Rules\\:\\:is_not_unique\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -9801,6 +9241,16 @@ 'count' => 1, 'path' => __DIR__ . '/system/Validation/StrictRules/Rules.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\StrictRules\\\\Rules\\:\\:field_exists\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Validation/StrictRules/Rules.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\StrictRules\\\\Rules\\:\\:field_exists\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Validation/StrictRules/Rules.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\StrictRules\\\\Rules\\:\\:greater_than\\(\\) has parameter \\$str with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -10016,6 +9466,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Validation/Validation.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\Validation\\:\\:run\\(\\) has parameter \\$dbGroup with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Validation/Validation.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\Validation\\:\\:setRule\\(\\) has parameter \\$errors with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -10111,6 +9566,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Validation/ValidationInterface.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\ValidationInterface\\:\\:run\\(\\) has parameter \\$dbGroup with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/system/Validation/ValidationInterface.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\ValidationInterface\\:\\:setRule\\(\\) has parameter \\$errors with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -10207,7 +9667,7 @@ 'path' => __DIR__ . '/system/View/Cells/Cell.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/system/Traits/PropertiesTrait\\.php\\:47\\:\\:getProperties\\(\\) return type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/system/Traits/PropertiesTrait\\.php\\:49\\:\\:getProperties\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/system/View/Cells/Cell.php', ]; @@ -10476,11 +9936,6 @@ 'count' => 2, 'path' => __DIR__ . '/system/View/View.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\Autoloader\\\\FatalLocator\\:\\:search\\(\\) return type has no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Autoloader/FatalLocator.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Cells\\\\StarterCell\\:\\:hello\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -10491,31 +9946,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/_support/Cells/StarterCell.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\LanguageCommand\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\LanguageCommand\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\LanguageCommand\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\LanguageCommand\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\LanguageCommand\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -10541,21 +9981,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', -]; $ignoreErrors[] = [ 'message' => '#^Property Tests\\\\Support\\\\Commands\\\\LanguageCommand\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -10563,7 +9988,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 3, + 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/LanguageCommand.php', ]; $ignoreErrors[] = [ @@ -10571,31 +9996,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/ParamsReveal.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\Unsuffixable\\:\\:execute\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\Unsuffixable\\:\\:generateClass\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\Unsuffixable\\:\\:generateClass\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\Unsuffixable\\:\\:generateView\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Commands\\\\Unsuffixable\\:\\:generateView\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -10621,31 +10031,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the left side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in &&, mixed given on the right side\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, mixed given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; $ignoreErrors[] = [ 'message' => '#^Property Tests\\\\Support\\\\Commands\\\\Unsuffixable\\:\\:\\$params type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/_support/Commands/Unsuffixable.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Tests\\\\Support\\\\Config\\\\BadRegistrar\\:\\:RegistrarConfig\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -10896,6 +10286,26 @@ 'count' => 1, 'path' => __DIR__ . '/tests/_support/Entity/Cast/CastPassParameters.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Entity\\\\CustomUser\\:\\:__construct\\(\\) has parameter \\$email with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Entity/CustomUser.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Entity\\\\CustomUser\\:\\:__get\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Entity/CustomUser.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Entity\\\\CustomUser\\:\\:reconstruct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Entity/CustomUser.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Unsafe usage of new static\\(\\)\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Entity/CustomUser.php', +]; $ignoreErrors[] = [ 'message' => '#^Property Tests\\\\Support\\\\Entity\\\\User\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -11101,6 +10511,26 @@ 'count' => 1, 'path' => __DIR__ . '/tests/_support/Models/UserModel.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Services\\\\Translation\\\\Nested\\\\TranslationFour\\:\\:list\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Services/Translation/TranslationNested/TranslationFour.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Services\\\\Translation\\\\TranslationOne\\:\\:list\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Services/Translation/TranslationOne.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Services\\\\Translation\\\\TranslationThree\\:\\:list\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Services/Translation/TranslationThree.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\Services\\\\Translation\\\\TranslationTwo\\:\\:list\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/Services/Translation/TranslationTwo.php', +]; $ignoreErrors[] = [ 'message' => '#^Property Tests\\\\Support\\\\SomeEntity\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -11199,250 +10629,385 @@ $ignoreErrors[] = [ 'message' => '#^Variable \\$message might not be defined\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/awesome_cell.php', + 'path' => __DIR__ . '/tests/_support/View/Cells/awesome_cell.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$this might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Cells/colors.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$greeting might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Cells/greeting.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$name might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Cells/greeting.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$items might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Cells/lister.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$value might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Cells/multiplier.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$message might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Cells/notice.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\OtherCells\\\\SampleClass\\:\\:hello\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/OtherCells/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:echobox\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:echobox\\(\\) has parameter \\$params with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:hello\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:index\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:staticEcho\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:staticEcho\\(\\) has parameter \\$params with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has parameter \\$p1 with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has parameter \\$p2 with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has parameter \\$p4 with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClassWithInitController\\:\\:index\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/SampleClassWithInitController.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$testString might not be defined\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/View/Views/simple.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method App\\\\Controllers\\\\Mycontroller\\:\\:getSomemethod\\(\\) has parameter \\$first with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/_controller/Mycontroller.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method App\\\\Controllers\\\\foo\\\\bar\\\\baz\\\\Some_controller\\:\\:some_method\\(\\) has parameter \\$first with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/_support/_controller/foo/bar/baz/Some_controller.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Assigning mixed directly on offset \'CONTENT_TYPE\' of \\$_SERVER is discouraged\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:createRequestAndResponse\\(\\) has parameter \\$userHeaders with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:invoke\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:invoke\\(\\) has parameter \\$args with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:makeController\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:makeController\\(\\) has parameter \\$userHeaders with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$this might not be defined\\.$#', + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:tryValidContentType\\(\\) has parameter \\$contentType with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/colors.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$greeting might not be defined\\.$#', + 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:tryValidContentType\\(\\) has parameter \\$mimeType with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/greeting.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$name might not be defined\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:__construct\\(\\) has parameter \\$formatter with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/greeting.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$items might not be defined\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/lister.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$value might not be defined\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/multiplier.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$message might not be defined\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Cells/notice.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\OtherCells\\\\SampleClass\\:\\:hello\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/OtherCells/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:echobox\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:echobox\\(\\) has parameter \\$params with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:hello\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:index\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:staticEcho\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:__construct\\(\\) has parameter \\$formatter with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:staticEcho\\(\\) has parameter \\$params with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has parameter \\$p1 with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has parameter \\$p2 with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClass\\:\\:work\\(\\) has parameter \\$p4 with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClass.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Tests\\\\Support\\\\View\\\\SampleClassWithInitController\\:\\:index\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/SampleClassWithInitController.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Variable \\$testString might not be defined\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/_support/View/Views/simple.php', + 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Assigning mixed directly on offset \'CONTENT_TYPE\' of \\$_SERVER is discouraged\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:invoke\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:__construct\\(\\) has parameter \\$formatter with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:invoke\\(\\) has parameter \\$args with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:makeController\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:makeController\\(\\) has parameter \\$userConfig with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:makeController\\(\\) has parameter \\$userHeaders with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:tryValidContentType\\(\\) has parameter \\$contentType with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\API\\\\ResponseTraitTest\\:\\:tryValidContentType\\(\\) has parameter \\$mimeType with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:__construct\\(\\) has parameter \\$formatter with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:__construct\\(\\) has parameter \\$request with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#', - 'count' => 1, + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:\\$formatter has no type specified\\.$#', + 'count' => 2, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:fail\\(\\) has parameter \\$messages with no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:\\$request has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:format\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:116\\:\\:\\$response has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:respond\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:\\$formatter has no type specified\\.$#', + 'count' => 2, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:respondCreated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:\\$request has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:respondDeleted\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:177\\:\\:\\$response has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:respondUpdated\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', - 'count' => 1, + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:\\$formatter has no type specified\\.$#', + 'count' => 2, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:\\$request has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:\\$request has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:561\\:\\:\\$response has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:299\\:\\:\\$response has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:\\$formatter has no type specified\\.$#', - 'count' => 2, + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:\\$format \\(\'html\'\\|\'json\'\\|\'xml\'\\|null\\) does not accept \'txt\'\\.$#', + 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:\\$request has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:\\$request has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:94\\:\\:\\$response has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/API/ResponseTraitTest\\.php\\:639\\:\\:\\$response has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/API/ResponseTraitTest.php', ]; @@ -11511,6 +11076,21 @@ 'count' => 2, 'path' => __DIR__ . '/tests/system/Autoloader/AutoloaderTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Call to an undefined method CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\:\\:__destruct\\(\\)\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Autoloader/FileLocatorCachedTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Call to an undefined method CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\:\\:deleteCache\\(\\)\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Autoloader/FileLocatorCachedTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Autoloader\\\\FileLocatorCachedTest\\:\\:testDeleteCache\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Autoloader/FileLocatorCachedTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Assigning 3 directly on offset \'argc\' of \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -11616,26 +11196,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Cache\\\\Handlers\\\\AbstractHandlerTest\\:\\:\\$dummy has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cache/Handlers/AbstractHandlerTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Cache\\\\Handlers\\\\AbstractHandlerTest\\:\\:\\$key1 has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cache/Handlers/AbstractHandlerTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Cache\\\\Handlers\\\\AbstractHandlerTest\\:\\:\\$key2 has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cache/Handlers/AbstractHandlerTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Cache\\\\Handlers\\\\AbstractHandlerTest\\:\\:\\$key3 has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cache/Handlers/AbstractHandlerTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Cache\\\\Handlers\\\\BaseHandlerTest\\:\\:provideValidateKeyInvalidType\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -11741,6 +11301,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Cache/ResponseCacheTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Cache\\\\ResponseCacheTest\\:\\:testCachePageIncomingRequestWithHttpMethods\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cache/ResponseCacheTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Cache\\\\ResponseCacheTest\\:\\:testInvalidCacheError\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -11761,6 +11326,11 @@ 'count' => 2, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Assigning \'/cannotFound\' directly on offset \'REQUEST_URI\' of \\$_SERVER is discouraged\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Assigning \'/cli\' directly on offset \'REQUEST_URI\' of \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -11778,12 +11348,12 @@ ]; $ignoreErrors[] = [ 'message' => '#^Assigning \'/index\\.php\' directly on offset \'SCRIPT_NAME\' of \\$_SERVER is discouraged\\.$#', - 'count' => 18, + 'count' => 20, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Assigning \'/pages/about\' directly on offset \'REQUEST_URI\' of \\$_SERVER is discouraged\\.$#', - 'count' => 7, + 'count' => 8, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ @@ -11798,7 +11368,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Assigning \'GET\' directly on offset \'REQUEST_METHOD\' of \\$_SERVER is discouraged\\.$#', - 'count' => 3, + 'count' => 4, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ @@ -11828,17 +11398,17 @@ ]; $ignoreErrors[] = [ 'message' => '#^Assigning 1 directly on offset \'argc\' of \\$_SERVER is discouraged\\.$#', - 'count' => 4, + 'count' => 5, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Assigning 2 directly on offset \'argc\' of \\$_SERVER is discouraged\\.$#', - 'count' => 28, + 'count' => 27, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Assigning array\\{\'index\\.php\', \'/\'\\} directly on offset \'argv\' of \\$_SERVER is discouraged\\.$#', - 'count' => 13, + 'count' => 12, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ @@ -11863,7 +11433,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Assigning array\\{\'index\\.php\'\\} directly on offset \'argv\' of \\$_SERVER is discouraged\\.$#', - 'count' => 4, + 'count' => 5, 'path' => __DIR__ . '/tests/system/CodeIgniterTest.php', ]; $ignoreErrors[] = [ @@ -11931,16 +11501,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Commands/CellGeneratorTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$search of function str_replace expects array\\|string, int given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Commands/ClearDebugbarTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, int given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Commands/ClearDebugbarTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', 'count' => 1, @@ -12031,6 +11591,36 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Commands/ScaffoldGeneratorTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinderTest\\:\\:getActualTranslationFourKeys\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Translation/LocalizationFinderTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinderTest\\:\\:getActualTranslationOneKeys\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Translation/LocalizationFinderTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Translation\\\\LocalizationFinderTest\\:\\:getActualTranslationThreeKeys\\(\\) return type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Translation/LocalizationFinderTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\ConfigCheckTest\\:\\:getBuffer\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Utilities/ConfigCheckTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\ConfigCheckTest\\:\\:testGetKintD\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Utilities/ConfigCheckTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\ConfigCheckTest\\:\\:testGetVarDump\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Utilities/ConfigCheckTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\NamespacesTest\\:\\:getBuffer\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -12056,6 +11646,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\AutoRouterImproved\\\\Controllers\\\\SubDir\\\\BlogController\\:\\:getSomeMethod\\(\\) has parameter \\$first with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\FilterFinderTest\\:\\:createFilters\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -12072,28 +11667,28 @@ 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/FilterFinderTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\FilterFinderTest\\:\\:\\$response \\(CodeIgniter\\\\HTTP\\\\Response\\) does not accept CodeIgniter\\\\HTTP\\\\ResponseInterface\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\FilterFinderTest\\:\\:testFilterOrder\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/FilterFinderTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\SampleURIGeneratorTest\\:\\:provideGet\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\FilterFinderTest\\:\\:testFilterOrderWithOldFilterOrder\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php', + 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/FilterFinderTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Accessing offset \'foo\' directly on \\$_SERVER is discouraged\\.$#', + 'message' => '#^Property CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\FilterFinderTest\\:\\:\\$response \\(CodeIgniter\\\\HTTP\\\\Response\\) does not accept CodeIgniter\\\\HTTP\\\\ResponseInterface\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/CommonFunctionsSendTest.php', + 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/FilterFinderTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$value of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int\\<1, max\\> given\\.$#', + 'message' => '#^Method CodeIgniter\\\\Commands\\\\Utilities\\\\Routes\\\\SampleURIGeneratorTest\\:\\:provideGet\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/CommonFunctionsSendTest.php', + 'path' => __DIR__ . '/tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int given\\.$#', - 'count' => 2, + 'message' => '#^Accessing offset \'foo\' directly on \\$_SERVER is discouraged\\.$#', + 'count' => 1, 'path' => __DIR__ . '/tests/system/CommonFunctionsSendTest.php', ]; $ignoreErrors[] = [ @@ -12151,16 +11746,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/CommonFunctionsTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$value of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int\\<1, max\\> given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/CommonFunctionsTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int given\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/system/CommonFunctionsTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Re\\-assigning arrays to \\$_GET directly is discouraged\\.$#', 'count' => 3, @@ -12216,11 +11801,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/CommonSingleServiceTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$expected of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) expects class\\-string\\, false given\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/system/CommonSingleServiceTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Accessing offset \'BAR\' directly on \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -12331,6 +11911,21 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Config/FactoriesTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Config\\\\FactoriesTest\\:\\:testGetCreatesConfigInstanceAndFactoriesConfigReturnsIt\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/FactoriesTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Config\\\\FactoriesTest\\:\\:testGetNonexistentClass\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/FactoriesTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Config\\\\FactoriesTest\\:\\:testGetReturnsFactoriesConfigInstance\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/FactoriesTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Config\\\\FactoriesTest\\:\\:testIsUpdated\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -12372,7 +11967,7 @@ 'path' => __DIR__ . '/tests/system/Config/FactoriesTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Config/FactoriesTest\\.php\\:86\\:\\:\\$widgets has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Config/FactoriesTest\\.php\\:88\\:\\:\\$widgets has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Config/FactoriesTest.php', ]; @@ -12524,110 +12119,190 @@ $ignoreErrors[] = [ 'message' => '#^Property SimpleConfig\\:\\:\\$onedeep has no type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', + 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property SimpleConfig\\:\\:\\$onedeep_value has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property SimpleConfig\\:\\:\\$password has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property SimpleConfig\\:\\:\\$second has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property SimpleConfig\\:\\:\\$shortie has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property SimpleConfig\\:\\:\\$simple has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Dead catch \\- CodeIgniter\\\\HTTP\\\\Exceptions\\\\RedirectException is never thrown in the try block\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/ControllerTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property class@anonymous/tests/system/ControllerTest\\.php\\:128\\:\\:\\$signup has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/ControllerTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property class@anonymous/tests/system/ControllerTest\\.php\\:128\\:\\:\\$signup_errors has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/ControllerTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property class@anonymous/tests/system/ControllerTest\\.php\\:151\\:\\:\\$signup has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/ControllerTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$cookies of class CodeIgniter\\\\Cookie\\\\CookieStore constructor expects array\\, array\\ given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieStoreTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Cookie\\\\CookieStoreTest\\:\\:\\$defaults type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieStoreTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Expression "\\$cookie\\[\'expiry\'\\]" on a separate line does not do anything\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:provideConfigPrefix\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:provideInvalidExpires\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:provideSetCookieHeaderCreation\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:testSetCookieHeaderCreation\\(\\) has parameter \\$changed with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property SimpleConfig\\:\\:\\$onedeep_value has no type specified\\.$#', + 'message' => '#^Parameter \\#3 \\$options of class CodeIgniter\\\\Cookie\\\\Cookie constructor expects array\\, array\\ given\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property SimpleConfig\\:\\:\\$password has no type specified\\.$#', + 'message' => '#^Property CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:\\$defaults type has no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', + 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property SimpleConfig\\:\\:\\$second has no type specified\\.$#', + 'message' => '#^Implicit array creation is not allowed \\- variable \\$array does not exist\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property SimpleConfig\\:\\:\\$shortie has no type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:createDataConverter\\(\\) has parameter \\$extractor with no signature specified for Closure\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property SimpleConfig\\:\\:\\$simple has no type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:createDataConverter\\(\\) has parameter \\$handlers with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Config/fixtures/SimpleConfig.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Dead catch \\- CodeIgniter\\\\HTTP\\\\Exceptions\\\\RedirectException is never thrown in the try block\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:createDataConverter\\(\\) has parameter \\$reconstructor with no signature specified for Closure\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/ControllerTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/ControllerTest\\.php\\:126\\:\\:\\$signup has no type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:createDataConverter\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/ControllerTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/ControllerTest\\.php\\:126\\:\\:\\$signup_errors has no type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:provideConvertDataFromDB\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/ControllerTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/ControllerTest\\.php\\:149\\:\\:\\$signup has no type specified\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:provideConvertDataToDB\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/ControllerTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$cookies of class CodeIgniter\\\\Cookie\\\\CookieStore constructor expects array\\, array\\ given\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testConvertDataFromDB\\(\\) has parameter \\$dbData with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieStoreTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Cookie\\\\CookieStoreTest\\:\\:\\$defaults type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testConvertDataFromDB\\(\\) has parameter \\$expected with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieStoreTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Expression "\\$cookie\\[\'expiry\'\\]" on a separate line does not do anything\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testConvertDataFromDB\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:provideConfigPrefix\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testConvertDataToDB\\(\\) has parameter \\$expected with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:provideInvalidExpires\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testConvertDataToDB\\(\\) has parameter \\$phpData with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:provideSetCookieHeaderCreation\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testConvertDataToDB\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:testSetCookieHeaderCreation\\(\\) has parameter \\$changed with no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testExtract\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$options of class CodeIgniter\\\\Cookie\\\\Cookie constructor expects array\\, array\\ given\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testExtractWithClosure\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property CodeIgniter\\\\Cookie\\\\CookieTest\\:\\:\\$defaults type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testExtractWithExtractMethod\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Cookie/CookieTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Access to an undefined property CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:\\$foobar\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testReconstructObjectWithClosure\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Access to protected property CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:\\$username\\.$#', + 'message' => '#^Method CodeIgniter\\\\DataConverter\\\\DataConverterTest\\:\\:testReconstructObjectWithReconstructMethod\\(\\) has no return type specified\\.$#', 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php', + 'path' => __DIR__ . '/tests/system/DataConverter/DataConverterTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Access to protected property class@anonymous/tests/system/Database/BaseConnectionTest\\.php\\:107\\:\\:\\$username\\.$#', + 'message' => '#^Access to an undefined property CodeIgniter\\\\Test\\\\Mock\\\\MockConnection\\:\\:\\$foobar\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php', ]; @@ -12646,11 +12321,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/BaseConnectionTest\\.php\\:107\\:\\:\\$returnValues has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/BaseConnectionTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Database\\\\BaseQueryTest\\:\\:provideHighlightQueryKeywords\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -12673,7 +12343,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, CodeIgniter\\\\Database\\\\ResultInterface\\|false given\\.$#', - 'count' => 3, + 'count' => 6, 'path' => __DIR__ . '/tests/system/Database/Builder/GetTest.php', ]; $ignoreErrors[] = [ @@ -12771,31 +12441,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/DatabaseTestCaseTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\Live\\\\AbstractGetFieldDataTest\\:\\:assertSameFieldData\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/Live/AbstractGetFieldDataTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\Live\\\\AbstractGetFieldDataTest\\:\\:assertSameFieldData\\(\\) has parameter \\$actual with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/Live/AbstractGetFieldDataTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\Live\\\\AbstractGetFieldDataTest\\:\\:assertSameFieldData\\(\\) has parameter \\$expected with no value type specified in iterable type array\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/Live/AbstractGetFieldDataTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\Live\\\\AbstractGetFieldDataTest\\:\\:createTableForDefault\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/Live/AbstractGetFieldDataTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\Live\\\\AbstractGetFieldDataTest\\:\\:createTableForType\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/Live/AbstractGetFieldDataTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Database\\\\Live\\\\ConnectTest\\:\\:\\$group1 has no type specified\\.$#', 'count' => 1, @@ -12847,37 +12492,37 @@ 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$deleted_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$deleted_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:232\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Database/Live/GetTest\\.php\\:256\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/GetTest.php', ]; @@ -12966,11 +12611,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Database/Live/SQLite3/AlterTableTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\Database\\\\Live\\\\SQLite3\\\\GetFieldDataTest\\:\\:createTableCompositePrimaryKey\\(\\) has no return type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Database/Live/SQLite3/GetFieldDataTest.php', -]; $ignoreErrors[] = [ '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\\.$#', 'count' => 1, @@ -13227,147 +12867,147 @@ 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1076\\:\\:getBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1078\\:\\:getBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1076\\:\\:getFakeBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1078\\:\\:getFakeBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1076\\:\\:setBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1078\\:\\:setBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1076\\:\\:setBar\\(\\) has parameter \\$value with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1078\\:\\:setBar\\(\\) has parameter \\$value with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:_getBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:_getBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:_setBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:_setBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:_setBar\\(\\) has parameter \\$value with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:_setBar\\(\\) has parameter \\$value with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:getBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:getBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:setBar\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:setBar\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:setBar\\(\\) has parameter \\$value with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:setBar\\(\\) has parameter \\$value with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1160\\:\\:getSimple\\(\\) has no return type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1162\\:\\:getSimple\\(\\) has no return type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1227\\:\\:setSeventh\\(\\) has parameter \\$seventh with no type specified\\.$#', + 'message' => '#^Method class@anonymous/tests/system/Entity/EntityTest\\.php\\:1229\\:\\:setSeventh\\(\\) has parameter \\$seventh with no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1076\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1078\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1076\\:\\:\\$original type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1078\\:\\:\\$original type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1114\\:\\:\\$original type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1116\\:\\:\\$original type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1160\\:\\:\\$_original has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1162\\:\\:\\$_original has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1160\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1162\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1190\\:\\:\\$_original has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1192\\:\\:\\$_original has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1190\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1192\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1209\\:\\:\\$_original has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1211\\:\\:\\$_original has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1209\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1211\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1227\\:\\:\\$_original has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1229\\:\\:\\$_original has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1227\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1229\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1285\\:\\:\\$_original has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1287\\:\\:\\$_original has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1285\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1287\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1314\\:\\:\\$_original has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1316\\:\\:\\$_original has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1314\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:1316\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:893\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Entity/EntityTest\\.php\\:895\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Entity/EntityTest.php', ]; @@ -13381,11 +13021,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Events/EventsTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Events/EventsTest\\.php\\:283\\:\\:\\$logged has no type specified\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Events/EventsTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Filters\\\\CSRFTest\\:\\:\\$response \\(CodeIgniter\\\\HTTP\\\\Response\\|null\\) does not accept CodeIgniter\\\\HTTP\\\\ResponseInterface\\.$#', 'count' => 2, @@ -13581,11 +13216,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/HTTP/CLIRequestTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\HTTP\\\\CLIRequestTest\\:\\:provideValidIPAddress\\(\\) return type has no value type specified in iterable type iterable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/HTTP/CLIRequestTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Re\\-assigning arrays to \\$_GET directly is discouraged\\.$#', 'count' => 1, @@ -14076,6 +13706,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/HTTP/MessageTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\HTTP\\\\MessageTest\\:\\:testSetHeaderWithExistingArrayValuesAppendArrayValue\\(\\) has parameter \\$arrayHeaderValue with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/HTTP/MessageTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\HTTP\\\\MessageTest\\:\\:testSetHeaderWithExistingArrayValuesAppendStringValue\\(\\) has parameter \\$arrayHeaderValue with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/HTTP/MessageTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$config of class CodeIgniter\\\\Log\\\\Logger constructor expects Config\\\\Logger, CodeIgniter\\\\Test\\\\Mock\\\\MockLogger given\\.$#', 'count' => 1, @@ -14136,11 +13776,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/HTTP/RequestTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method CodeIgniter\\\\HTTP\\\\RequestTest\\:\\:provideValidIPAddress\\(\\) return type has no value type specified in iterable type iterable\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/HTTP/RequestTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Re\\-assigning arrays to \\$_GET directly is discouraged\\.$#', 'count' => 1, @@ -14152,15 +13787,10 @@ 'path' => __DIR__ . '/tests/system/HTTP/ResponseCookieTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$value of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int\\<1, max\\> given\\.$#', + 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects int, string given\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/HTTP/ResponseSendTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int given\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/tests/system/HTTP/ResponseSendTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Accessing offset \'SERVER_SOFTWARE\' directly on \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -14217,12 +13847,7 @@ 'path' => __DIR__ . '/tests/system/HTTP/ResponseTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$value of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int\\<1, max\\> given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/HTTP/ResponseTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int given\\.$#', + 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects int, string given\\.$#', 'count' => 2, 'path' => __DIR__ . '/tests/system/HTTP/ResponseTest.php', ]; @@ -14791,6 +14416,26 @@ 'count' => 2, 'path' => __DIR__ . '/tests/system/HTTP/UserAgentTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelperDotKeyExistsTest\\:\\:\\$array type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelperRecursiveDiffTest\\:\\:\\$compareWith type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Helpers/Array/ArrayHelperRecursiveDiffTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelperSortValuesByNaturalTest\\:\\:\\$arrayWithArrayValues type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Helpers\\\\Array\\\\ArrayHelperSortValuesByNaturalTest\\:\\:\\$arrayWithStringValues type has no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Helpers\\\\ArrayHelperTest\\:\\:provideArrayDeepSearch\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -14906,26 +14551,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Helpers/ArrayHelperTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of function set_cookie expects string, int given\\.$#', - 'count' => 4, - 'path' => __DIR__ . '/tests/system/Helpers/CookieHelperTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Helpers/CookieHelperTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#7 \\$secure of function set_cookie expects bool\\|null, string given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Helpers/CookieHelperTest.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#8 \\$httpOnly of function set_cookie expects bool\\|null, string given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Helpers/CookieHelperTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Helpers\\\\CookieHelperTest\\:\\:\\$response \\(CodeIgniter\\\\HTTP\\\\Response\\) does not accept CodeIgniter\\\\HTTP\\\\ResponseInterface\\.$#', 'count' => 1, @@ -14941,11 +14566,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Helpers/FilesystemHelperTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$directoryDepth of function directory_map expects int, false given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Helpers/FilesystemHelperTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Helpers\\\\FilesystemHelperTest\\:\\:\\$structure type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -14991,11 +14611,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Helpers/NumberHelperTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$first of function increment_string expects int, string given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Helpers/TextHelperTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Assigning \'/\' directly on offset \'REQUEST_URI\' of \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -15341,11 +14956,6 @@ 'count' => 3, 'path' => __DIR__ . '/tests/system/Honeypot/HoneypotTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Property Config\\\\Honeypot\\:\\:\\$hidden \\(bool\\) does not accept string\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Honeypot/HoneypotTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\HotReloader\\\\DirectoryHasherTest\\:\\:testHash\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -15527,7 +15137,7 @@ 'path' => __DIR__ . '/tests/system/Log/LoggerTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$message of method CodeIgniter\\\\Log\\\\Logger\\:\\:log\\(\\) expects string, CodeIgniter\\\\Test\\\\Mock\\\\MockLogger given\\.$#', + 'message' => '#^Parameter \\#2 \\$message of method CodeIgniter\\\\Log\\\\Logger\\:\\:log\\(\\) expects string\\|Stringable, CodeIgniter\\\\Test\\\\Mock\\\\MockLogger given\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Log/LoggerTest.php', ]; @@ -15536,11 +15146,51 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Log/LoggerTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Result of method CodeIgniter\\\\Log\\\\Logger\\:\\:log\\(\\) \\(void\\) is used\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Log/LoggerTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Call to an undefined method CodeIgniter\\\\Model\\:\\:affectedRows\\(\\)\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/AffectedRowsTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'email\' does not exist on array\\\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'email\' does not exist on array\\{\\}\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'id\' does not exist on array\\{email\\: array\\{\'private@example\\.org\'\\}\\}\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Offset \'id\' does not exist on array\\{\\}\\.$#', + 'count' => 4, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$row of method CodeIgniter\\\\BaseModel\\:\\:save\\(\\) expects array\\\\|object, array\\\\> given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#2 \\$row of method CodeIgniter\\\\Model\\:\\:update\\(\\) expects array\\\\|object\\|null, array\\\\> given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Models\\\\DataConverterModelTest\\:\\:\\$seed \\(array\\\\>\\|class\\-string\\\\) does not accept default value of type string\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Models/DataConverterModelTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Models\\\\DeleteModelTest\\:\\:emptyPkValues\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -15617,82 +15267,82 @@ 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$deleted has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$deleted has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:191\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:193\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$deleted has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$deleted has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:285\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/InsertModelTest\\.php\\:287\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/InsertModelTest.php', ]; @@ -15717,7 +15367,7 @@ 'path' => __DIR__ . '/tests/system/Models/PaginateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Call to an undefined method class@anonymous/tests/system/Models/SaveModelTest\\.php\\:286\\:\\:truncate\\(\\)\\.$#', + 'message' => '#^Call to an undefined method class@anonymous/tests/system/Models/SaveModelTest\\.php\\:288\\:\\:truncate\\(\\)\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; @@ -15737,72 +15387,72 @@ 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$deleted has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$deleted has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:237\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:239\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:270\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:272\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:270\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:272\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:270\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:272\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:270\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:272\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:270\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:272\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:286\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/SaveModelTest\\.php\\:288\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/SaveModelTest.php', ]; @@ -15872,122 +15522,122 @@ 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$deleted has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$deleted has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:193\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:197\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$deleted has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$deleted has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:212\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:216\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$_options has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$_options has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$country has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$country has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$created_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$created_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$deleted has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$deleted has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$email has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$email has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$id has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$id has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$name has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$name has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:345\\:\\:\\$updated_at has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/UpdateModelTest\\.php\\:349\\:\\:\\$updated_at has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; @@ -15997,7 +15647,7 @@ 'path' => __DIR__ . '/tests/system/Models/UpdateModelTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/Models/ValidationModelTest\\.php\\:243\\:\\:\\$grouptest has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/Models/ValidationModelTest\\.php\\:245\\:\\:\\$grouptest has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/Models/ValidationModelTest.php', ]; @@ -16331,6 +15981,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouterImprovedTest\\:\\:provideRejectTranslateUriToCamelCase\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouterImprovedTest\\:\\:provideTranslateUriToCamelCase\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouterImprovedTest\\:\\:testAutoRouteFallbackToDefaultControllerNoParams\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -16371,6 +16031,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouterImprovedTest\\:\\:testRejectTranslateUriToCamelCase\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouterImprovedTest\\:\\:testRejectsURIWithUnderscoreController\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -16386,6 +16051,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\AutoRouterImprovedTest\\:\\:testTranslateUriToCamelCase\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/AutoRouterImprovedTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\Controllers\\\\BlogController\\:\\:getSomeMethod\\(\\) has parameter \\$first with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/Controllers/BlogController.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\Controllers\\\\Index\\:\\:getIndex\\(\\) has parameter \\$p1 with no type specified\\.$#', 'count' => 1, @@ -16401,6 +16076,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Router/Controllers/Remap.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\Controllers\\\\SubDir\\\\BlogController\\:\\:getSomeMethod\\(\\) has parameter \\$first with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/Controllers/SubDir/BlogController.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\Controllers\\\\Subfolder\\\\Home\\:\\:getIndex\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -16416,6 +16096,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Router/Controllers/Subfolder/Home.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Router\\\\Controllers\\\\Subfolder\\\\Sub\\\\BlogController\\:\\:getSomeMethod\\(\\) has parameter \\$first with no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Router/Controllers/Subfolder/Sub/BlogController.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Router\\\\Controllers\\\\Subfolder\\\\Sub\\\\Mycontroller\\:\\:getSomemethod\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -16561,6 +16246,21 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Router/RouterTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIniTest\\:\\:testCheckIni\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Security/CheckPhpIniTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIniTest\\:\\:testRunCli\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Security/CheckPhpIniTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Security\\\\CheckPhpIniTest\\:\\:testRunWeb\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Security/CheckPhpIniTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Assigning \'POST\' directly on offset \'REQUEST_METHOD\' of \\$_SERVER is discouraged\\.$#', 'count' => 1, @@ -16681,6 +16381,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Session/Handlers/Database/RedisHandlerTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Session\\\\Handlers\\\\Database\\\\RedisHandlerTest\\:\\:provideSetSavePath\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Session/Handlers/Database/RedisHandlerTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Session\\\\Handlers\\\\Database\\\\RedisHandlerTest\\:\\:testSetSavePath\\(\\) has parameter \\$expected with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Session/Handlers/Database/RedisHandlerTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Assigning \'xmlhttprequest\' directly on offset \'HTTP_X_REQUESTED_WITH\' of \\$_SERVER is discouraged\\.$#', 'count' => 2, @@ -16786,6 +16496,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Test/ControllerTestTraitTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\Test\\\\ControllerTestTraitTest\\:\\:\\$uri \\(string\\) does not accept CodeIgniter\\\\HTTP\\\\SiteURI\\.$#', + 'count' => 2, + 'path' => __DIR__ . '/tests/system/Test/ControllerTestTraitTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Test\\\\DOMParserTest\\:\\:provideText\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -17111,11 +16826,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Test/TestResponseTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$expire of method CodeIgniter\\\\HTTP\\\\Response\\:\\:setCookie\\(\\) expects string, int\\<1, max\\> given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Test/TestResponseTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Throttle\\\\ThrottleTest\\:\\:provideTokenTimeCalculationUCs\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -17141,11 +16851,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Validation/CreditCardRulesTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$prefix of static method CodeIgniter\\\\Validation\\\\CreditCardRulesTest\\:\\:generateCardNumber\\(\\) expects int, string given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Validation/CreditCardRulesTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Validation\\\\CreditCardRulesTest\\:\\:\\$config type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -17356,6 +17061,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Validation/RulesTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\RulesTest\\:\\:provideFieldExists\\(\\) return type has no value type specified in iterable type iterable\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Validation/RulesTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\RulesTest\\:\\:provideGreaterThan\\(\\) return type has no value type specified in iterable type iterable\\.$#', 'count' => 1, @@ -17456,6 +17166,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Validation/RulesTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\RulesTest\\:\\:testFieldExists\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Validation/RulesTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\RulesTest\\:\\:testFieldExists\\(\\) has parameter \\$rules with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Validation/RulesTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\RulesTest\\:\\:testIfExist\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -17531,11 +17251,6 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Validation/StrictRules/CreditCardRulesTest.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$prefix of static method CodeIgniter\\\\Validation\\\\StrictRules\\\\CreditCardRulesTest\\:\\:generateCardNumber\\(\\) expects int, string given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/tests/system/Validation/StrictRules/CreditCardRulesTest.php', -]; $ignoreErrors[] = [ 'message' => '#^Property CodeIgniter\\\\Validation\\\\StrictRules\\\\CreditCardRulesTest\\:\\:\\$config type has no value type specified in iterable type array\\.$#', 'count' => 1, @@ -17701,6 +17416,21 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/Validation/ValidationTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\ValidationTest\\:\\:rule1\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Validation/ValidationTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\ValidationTest\\:\\:rule2\\(\\) has no return type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Validation/ValidationTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method CodeIgniter\\\\Validation\\\\ValidationTest\\:\\:rule2\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/Validation/ValidationTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\Validation\\\\ValidationTest\\:\\:testIfExistRuleWithAsterisk\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#', 'count' => 1, @@ -17766,6 +17496,16 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/View/CellTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\View\\\\DecoratorsTest\\:\\:\\$loader \\(CodeIgniter\\\\Autoloader\\\\FileLocator\\) does not accept CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/View/DecoratorsTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\View\\\\ParserFilterTest\\:\\:\\$loader \\(CodeIgniter\\\\Autoloader\\\\FileLocator\\) does not accept CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/View/ParserFilterTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method CodeIgniter\\\\View\\\\ParserPluginTest\\:\\:setHints\\(\\) has no return type specified\\.$#', 'count' => 1, @@ -17787,7 +17527,7 @@ 'path' => __DIR__ . '/tests/system/View/ParserTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method class@anonymous/tests/system/View/ParserTest\\.php\\:337\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#', + 'message' => '#^Method class@anonymous/tests/system/View/ParserTest\\.php\\:339\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/View/ParserTest.php', ]; @@ -17797,17 +17537,22 @@ 'path' => __DIR__ . '/tests/system/View/ParserTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/View/ParserTest\\.php\\:337\\:\\:\\$bar has no type specified\\.$#', + 'message' => '#^Property CodeIgniter\\\\View\\\\ParserTest\\:\\:\\$loader \\(CodeIgniter\\\\Autoloader\\\\FileLocator\\) does not accept CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/View/ParserTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/View/ParserTest\\.php\\:337\\:\\:\\$foo has no type specified\\.$#', + 'message' => '#^Property class@anonymous/tests/system/View/ParserTest\\.php\\:339\\:\\:\\$bar has no type specified\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/View/ParserTest.php', ]; $ignoreErrors[] = [ - 'message' => '#^Property class@anonymous/tests/system/View/ParserTest\\.php\\:367\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', + 'message' => '#^Property class@anonymous/tests/system/View/ParserTest\\.php\\:339\\:\\:\\$foo has no type specified\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/View/ParserTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Property class@anonymous/tests/system/View/ParserTest\\.php\\:369\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#', 'count' => 1, 'path' => __DIR__ . '/tests/system/View/ParserTest.php', ]; @@ -17896,6 +17641,11 @@ 'count' => 1, 'path' => __DIR__ . '/tests/system/View/ViewTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Property CodeIgniter\\\\View\\\\ViewTest\\:\\:\\$loader \\(CodeIgniter\\\\Autoloader\\\\FileLocator\\) does not accept CodeIgniter\\\\Autoloader\\\\FileLocatorInterface\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/tests/system/View/ViewTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$node \\(PhpParser\\\\Node\\\\Stmt\\) of method Utils\\\\PHPStan\\\\CheckUseStatementsAfterLicenseRule\\:\\:processNode\\(\\) should be contravariant with parameter \\$node \\(PhpParser\\\\Node\\) of method PHPStan\\\\Rules\\\\Rule\\\\:\\:processNode\\(\\)$#', 'count' => 1, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8735975fa74c..45b6b07231c4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,24 +1,32 @@ - - + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" + bootstrap="system/Test/bootstrap.php" backupGlobals="false" + beStrictAboutOutputDuringTests="true" colors="true" columns="max" + failOnRisky="true" failOnWarning="true" + cacheDirectory=".phpunit.cache"> + + + + + + + + + + + + + + + + tests/system + + + system - system/Commands/Generators/Views system/Debug/Toolbar/Views @@ -32,39 +40,7 @@ system/Test/ControllerTester.php system/Test/FeatureTestCase.php - - - - - - - - - tests/system - - - - - - - - - 0.50 - - - 30 - - - 2 - - - false - - - - - - + diff --git a/preload.php b/preload.php index 63c781c220c5..2fa699388270 100644 --- a/preload.php +++ b/preload.php @@ -49,11 +49,12 @@ class preload */ private array $paths = [ [ - 'include' => __DIR__ . '/vendor/codeigniter4/framework/system', + 'include' => __DIR__ . '/vendor/codeigniter4/framework/system', // Change this path if using manual installation 'exclude' => [ // Not needed if you don't use them. '/system/Database/OCI8/', '/system/Database/Postgre/', + '/system/Database/SQLite3/', '/system/Database/SQLSRV/', // Not needed. '/system/Database/Seeder.php', diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 8c4f4c696912..53d1b6ce876a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -85,11 +85,6 @@ - - - dom = &$this->domParser]]> - - |parser_callable_string|parser_callable>]]> diff --git a/psalm_autoload.php b/psalm_autoload.php index d42636a11fef..b973cc88bc7d 100644 --- a/psalm_autoload.php +++ b/psalm_autoload.php @@ -25,6 +25,7 @@ $dirs = [ 'tests/_support/Controllers', + 'tests/_support/_controller', 'tests/system/Config/fixtures', ]; diff --git a/public/index.php b/public/index.php index 1cc4710549d5..5ec58a7729c3 100644 --- a/public/index.php +++ b/public/index.php @@ -1,7 +1,12 @@ systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; - -// Load environment settings from .env files into $_SERVER and $_ENV -require_once SYSTEMPATH . 'Config/DotEnv.php'; -(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); - -// Define ENVIRONMENT -if (! defined('ENVIRONMENT')) { - define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); -} - -// Load Config Cache -// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); -// $factoriesCache->load('config'); -// ^^^ Uncomment these lines if you want to use Config Caching. - -/* - * --------------------------------------------------------------- - * GRAB OUR CODEIGNITER INSTANCE - * --------------------------------------------------------------- - * - * The CodeIgniter class contains the core functionality to make - * the application run, and does all the dirty work to get - * the pieces all working together. - */ - -$app = Config\Services::codeigniter(); -$app->initialize(); -$context = is_cli() ? 'php-cli' : 'web'; -$app->setContext($context); - -/* - *--------------------------------------------------------------- - * LAUNCH THE APPLICATION - *--------------------------------------------------------------- - * Now that everything is set up, it's time to actually fire - * up the engines and make this app do its thang. - */ - -$app->run(); - -// Save Config Cache -// $factoriesCache->save('config'); -// ^^^ Uncomment this line if you want to use Config Caching. +// LOAD THE FRAMEWORK BOOTSTRAP FILE +require $paths->systemDirectory . '/Boot.php'; -// Exits the application, setting the exit code for CLI-based applications -// that might be watching. -exit(EXIT_SUCCESS); +exit(CodeIgniter\Boot::bootWeb($paths)); diff --git a/rector.php b/rector.php index e52be465b4e5..b9d51f872fdc 100644 --- a/rector.php +++ b/rector.php @@ -1,5 +1,7 @@ sets([ SetList::DEAD_CODE, - LevelSetList::UP_TO_PHP_74, + LevelSetList::UP_TO_PHP_81, PHPUnitSetList::PHPUNIT_CODE_QUALITY, PHPUnitSetList::PHPUNIT_100, ]); @@ -90,13 +104,21 @@ __DIR__ . '/system/ThirdParty', __DIR__ . '/tests/system/Config/fixtures', __DIR__ . '/tests/system/Filters/fixtures', - __DIR__ . '/tests/_support', + __DIR__ . '/tests/_support/Commands/Foobar.php', + __DIR__ . '/tests/_support/View', + JsonThrowOnErrorRector::class, YieldDataProviderRector::class, + RemoveUnusedPromotedPropertyRector::class => [ + // Bug in rector 1.0.0. See https://github.com/rectorphp/rector-src/pull/5573 + __DIR__ . '/tests/_support/Entity/CustomUser.php', + ], + RemoveUnusedPrivateMethodRector::class => [ // private method called via getPrivateMethodInvoker __DIR__ . '/tests/system/Test/ReflectionHelperTest.php', + __DIR__ . '/tests/_support/Test/TestForReflectionHelper.php', ], RemoveUnusedConstructorParamRector::class => [ @@ -106,6 +128,7 @@ __DIR__ . '/system/Router/AutoRouterImproved.php', // @TODO remove if deprecated $config is removed __DIR__ . '/system/HTTP/Request.php', + __DIR__ . '/system/HTTP/Response.php', ], // check on constant compare @@ -113,21 +136,86 @@ __DIR__ . '/system/Autoloader/Autoloader.php', ], - // session handlers have the gc() method with underscored parameter `$max_lifetime` UnderscoreToCamelCaseVariableNameRector::class => [ + // session handlers have the gc() method with underscored parameter `$max_lifetime` __DIR__ . '/system/Session/Handlers', + __DIR__ . '/tests/_support/Entity/CustomUser.php', + ], + + DeclareStrictTypesRector::class => [ + __DIR__ . '/app', + __DIR__ . '/system/CodeIgniter.php', + __DIR__ . '/system/Config/BaseConfig.php', + __DIR__ . '/system/Commands/Generators/Views', + __DIR__ . '/system/Pager/Views', + __DIR__ . '/system/Test/ControllerTestTrait.php', + __DIR__ . '/system/Validation/Views', + __DIR__ . '/system/View/Parser.php', + __DIR__ . '/tests/system/Debug/ExceptionsTest.php', + __DIR__ . '/tests/system/View/Views', ], // use mt_rand instead of random_int on purpose on non-cryptographically random RandomFunctionRector::class, SimplifyRegexPatternRector::class, + + // PHP 8.0 features but cause breaking changes + ClassPropertyAssignToConstructorPromotionRector::class => [ + __DIR__ . '/system/Database/BaseResult.php', + __DIR__ . '/system/Database/RawSql.php', + __DIR__ . '/system/Debug/BaseExceptionHandler.php', + __DIR__ . '/system/Filters/Filters.php', + __DIR__ . '/system/HTTP/CURLRequest.php', + __DIR__ . '/system/HTTP/DownloadResponse.php', + __DIR__ . '/system/HTTP/IncomingRequest.php', + __DIR__ . '/system/Security/Security.php', + __DIR__ . '/system/Session/Session.php', + ], + MixedTypeRector::class, + + // PHP 8.1 features but cause breaking changes + FinalizePublicClassConstantRector::class => [ + __DIR__ . '/system/Cache/Handlers/BaseHandler.php', + __DIR__ . '/system/Cache/Handlers/FileHandler.php', + __DIR__ . '/system/CodeIgniter.php', + __DIR__ . '/system/Events/Events.php', + __DIR__ . '/system/Log/Handlers/ChromeLoggerHandler.php', + __DIR__ . '/system/Log/Handlers/ErrorlogHandler.php', + __DIR__ . '/system/Security/Security.php', + ], + ReturnNeverTypeRector::class => [ + __DIR__ . '/system/Cache/Handlers/BaseHandler.php', + __DIR__ . '/system/Cache/Handlers/MemcachedHandler.php', + __DIR__ . '/system/Cache/Handlers/WincacheHandler.php', + __DIR__ . '/system/CodeIgniter.php', + __DIR__ . '/system/Database/MySQLi/Utils.php', + __DIR__ . '/system/Database/OCI8/Utils.php', + __DIR__ . '/system/Database/Postgre/Utils.php', + __DIR__ . '/system/Database/SQLSRV/Utils.php', + __DIR__ . '/system/Database/SQLite3/Utils.php', + __DIR__ . '/system/HTTP/DownloadResponse.php', + __DIR__ . '/system/HTTP/SiteURI.php', + __DIR__ . '/system/Helpers/kint_helper.php', + __DIR__ . '/tests/_support/Autoloader/FatalLocator.php', + ], + + // Unnecessary (string) is inserted + NullToStrictStringFuncCallArgRector::class, + + // PHPUnit 10 (requires PHP 8.1) features + DataProviderAnnotationToAttributeRector::class, + DependsAnnotationWithValueToAttributeRector::class, + AnnotationWithValueToAttributeRector::class, + AnnotationToAttributeRector::class, + CoversAnnotationWithValueToAttributeRector::class, ]); // auto import fully qualified class names $rectorConfig->importNames(); $rectorConfig->removeUnusedImports(); + $rectorConfig->rule(DeclareStrictTypesRector::class); $rectorConfig->rule(UnderscoreToCamelCaseVariableNameRector::class); $rectorConfig->rule(SimplifyUselessVariableRector::class); $rectorConfig->rule(RemoveAlwaysElseRector::class); diff --git a/spark b/spark index 9daa44034473..a56fbc1bd7b6 100755 --- a/spark +++ b/spark @@ -12,13 +12,16 @@ /* * -------------------------------------------------------------------- - * CodeIgniter command-line tools + * CODEIGNITER COMMAND-LINE TOOLS * -------------------------------------------------------------------- * The main entry point into the CLI system and allows you to run * commands and perform maintenance on your application. - * - * Because CodeIgniter can handle CLI requests as just another web request - * this class mainly acts as a passthru to the framework itself. + */ + +/* + *--------------------------------------------------------------- + * CHECK SERVER API + *--------------------------------------------------------------- */ // Refuse to run when called from php-cgi @@ -26,8 +29,13 @@ if (strpos(PHP_SAPI, 'cgi') === 0) { exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); } -// Check PHP version. -$minPhpVersion = '7.4'; // If you update this, don't forget to update `public/index.php`. +/* + *--------------------------------------------------------------- + * CHECK PHP VERSION + *--------------------------------------------------------------- + */ + +$minPhpVersion = '8.1'; // If you update this, don't forget to update `public/index.php`. if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { $message = sprintf( 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s', @@ -42,12 +50,11 @@ if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { error_reporting(E_ALL); ini_set('display_errors', '1'); -/** - * @var bool - * - * @deprecated No longer in use. `CodeIgniter` has `$context` property. +/* + *--------------------------------------------------------------- + * SET THE CURRENT DIRECTORY + *--------------------------------------------------------------- */ -define('SPARKED', true); // Path to the front controller define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); @@ -64,41 +71,14 @@ chdir(FCPATH); * and fires up an environment-specific bootstrapping. */ -// Load our paths config file +// LOAD OUR PATHS CONFIG FILE // This is the line that might need to be changed, depending on your folder structure. require FCPATH . '../app/Config/Paths.php'; // ^^^ Change this line if you move your application folder $paths = new Config\Paths(); -// Location of the framework bootstrap file. -require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; - -// Load environment settings from .env files into $_SERVER and $_ENV -require_once SYSTEMPATH . 'Config/DotEnv.php'; -(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); - -// Define ENVIRONMENT -if (! defined('ENVIRONMENT')) { - define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); -} - -// Grab our CodeIgniter -$app = Config\Services::codeigniter(); -$app->initialize(); - -// Grab our Console -$console = new CodeIgniter\CLI\Console(); - -// Show basic information before we do anything else. -if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { - unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore - $suppress = true; -} - -$console->showHeader($suppress); - -// fire off the command in the main framework. -$exit = $console->run(); +// LOAD THE FRAMEWORK BOOTSTRAP FILE +require $paths->systemDirectory . '/Boot.php'; -exit(is_int($exit) ? $exit : EXIT_SUCCESS); +exit(CodeIgniter\Boot::bootSpark($paths)); diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 83a14528f6c3..01ce42860b32 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -1,5 +1,7 @@ response->getHeaderLine('Content-Type'); - $contentType = str_replace('application/json', 'text/html', $contentType); - $contentType = str_replace('application/', 'text/', $contentType); - $this->response->setContentType($contentType); - $this->format = 'html'; - - return $data; - } + $format = service('format'); - $format = Services::format(); - $mime = "application/{$this->format}"; + $mime = ($this->format === null) ? $format->getConfig()->supportedResponseFormats[0] + : "application/{$this->format}"; // Determine correct response type through content negotiation if not explicitly declared if ( @@ -336,6 +331,23 @@ protected function format($data = null) $this->formatter = $format->getFormatter($mime); } + $asHtml = $this->stringAsHtml ?? false; + + // Returns as HTML. + if ( + ($mime === 'application/json' && $asHtml && is_string($data)) + || ($mime !== 'application/json' && is_string($data)) + ) { + // The content type should be text/... and not application/... + $contentType = $this->response->getHeaderLine('Content-Type'); + $contentType = str_replace('application/json', 'text/html', $contentType); + $contentType = str_replace('application/', 'text/', $contentType); + $this->response->setContentType($contentType); + $this->format = 'html'; + + return $data; + } + if ($mime !== 'application/json') { // Recursively convert objects into associative arrays // Conversion not required for JSONFormatter @@ -348,11 +360,14 @@ protected function format($data = null) /** * Sets the format the response should be in. * + * @param string|null $format Response format + * @phpstan-param 'json'|'xml' $format + * * @return $this */ protected function setResponseFormat(?string $format = null) { - $this->format = strtolower($format); + $this->format = ($format === null) ? null : strtolower($format); return $this; } diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 088c850efa58..78d31eae0d7f 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -1,5 +1,7 @@ + * @var array */ protected $classmap = []; @@ -139,8 +146,6 @@ private function loadComposerAutoloader(Modules $modules): void /** @var ClassLoader $composer */ $composer = include COMPOSER_PATH; - $this->loadComposerClassmap($composer); - // Should we load through Composer's namespaces, also? if ($modules->discoverInComposer) { // @phpstan-ignore-next-line @@ -157,11 +162,11 @@ private function loadComposerAutoloader(Modules $modules): void */ public function register() { - // Prepend the PSR4 autoloader for maximum performance. - spl_autoload_register([$this, 'loadClass'], true, true); + // Register classmap loader for the files in our class map. + spl_autoload_register($this->loadClassmap(...), true); - // Now prepend another loader for the files in our class map. - spl_autoload_register([$this, 'loadClassmap'], true, true); + // Register the PSR-4 autoloader. + spl_autoload_register($this->loadClass(...), true); // Load our non-class files foreach ($this->files as $file) { @@ -176,8 +181,8 @@ public function register() */ public function unregister(): void { - spl_autoload_unregister([$this, 'loadClass']); - spl_autoload_unregister([$this, 'loadClassmap']); + spl_autoload_unregister($this->loadClass(...)); + spl_autoload_unregister($this->loadClassmap(...)); } /** @@ -215,7 +220,8 @@ public function addNamespace($namespace, ?string $path = null) * * If a prefix param is set, returns only paths to the given prefix. * - * @return array + * @return array>|list + * @phpstan-return ($prefix is null ? array> : list) */ public function getNamespace(?string $prefix = null) { @@ -275,12 +281,12 @@ public function loadClass(string $class): void */ protected function loadInNamespace(string $class) { - if (strpos($class, '\\') === false) { + if (! str_contains($class, '\\')) { return false; } foreach ($this->prefixes as $namespace => $directories) { - if (strpos($class, $namespace) === 0) { + if (str_starts_with($class, $namespace)) { $relativeClassPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen($namespace))); foreach ($directories as $directory) { @@ -346,7 +352,7 @@ public function sanitizeFilename(string $filename): string ); } if ($result === false) { - $message = PHP_VERSION_ID >= 80000 ? preg_last_error_msg() : 'Regex error. error code: ' . preg_last_error(); + $message = preg_last_error_msg(); throw new RuntimeException($message . '. filename: "' . $filename . '"'); } @@ -365,9 +371,13 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa { $namespacePaths = $composer->getPrefixesPsr4(); - // Get rid of CodeIgniter so we don't have duplicates - if (isset($namespacePaths['CodeIgniter\\'])) { - unset($namespacePaths['CodeIgniter\\']); + // Get rid of duplicated namespaces. + $duplicatedNamespaces = ['CodeIgniter', APP_NAMESPACE, 'Config']; + + foreach ($duplicatedNamespaces as $ns) { + if (isset($namespacePaths[$ns . '\\'])) { + unset($namespacePaths[$ns . '\\']); + } } if (! method_exists(InstalledVersions::class, 'getAllRawData')) { @@ -415,7 +425,7 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa foreach ($srcPaths as $path) { foreach ($installPaths as $installPath) { - if ($installPath === substr($path, 0, strlen($installPath))) { + if (str_starts_with($path, $installPath)) { $add = true; break 2; } @@ -431,13 +441,6 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa $this->addNamespace($newPaths); } - private function loadComposerClassmap(ClassLoader $composer): void - { - $classes = $composer->getClassMap(); - - $this->classmap = array_merge($this->classmap, $classes); - } - /** * Locates autoload information from Composer, if available. * @@ -483,4 +486,76 @@ public function loadHelpers(): void { helper($this->helpers); } + + /** + * Initializes Kint + */ + public function initializeKint(bool $debug = false): void + { + if ($debug) { + $this->autoloadKint(); + $this->configureKint(); + } elseif (class_exists(Kint::class)) { + // In case that Kint is already loaded via Composer. + Kint::$enabled_mode = false; + } + + helper('kint'); + } + + 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) { + $class = explode('\\', $class); + + if (array_shift($class) !== 'Kint') { + return; + } + + $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php'; + + if (is_file($file)) { + require_once $file; + } + }); + + require_once SYSTEMPATH . 'ThirdParty/Kint/init.php'; + } + } + + private function configureKint(): void + { + $config = new KintConfig(); + + Kint::$depth_limit = $config->maxDepth; + Kint::$display_called_from = $config->displayCalledFrom; + Kint::$expanded = $config->expanded; + + if (isset($config->plugins) && is_array($config->plugins)) { + Kint::$plugins = $config->plugins; + } + + $csp = Services::csp(); + if ($csp->enabled()) { + RichRenderer::$js_nonce = $csp->getScriptNonce(); + RichRenderer::$css_nonce = $csp->getStyleNonce(); + } + + RichRenderer::$theme = $config->richTheme; + RichRenderer::$folder = $config->richFolder; + RichRenderer::$sort = $config->richSort; + if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) { + RichRenderer::$value_plugins = $config->richObjectPlugins; + } + if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) { + RichRenderer::$tab_plugins = $config->richTabPlugins; + } + + CliRenderer::$cli_colors = $config->cliColors; + CliRenderer::$force_utf8 = $config->cliForceUTF8; + CliRenderer::$detect_width = $config->cliDetectWidth; + CliRenderer::$min_terminal_width = $config->cliMinWidth; + } } diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 6f1c547d2358..97da0fa275eb 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -1,5 +1,7 @@ ensureExt($file, $ext); // Clears the folder name if it is at the beginning of the filename - if ($folder !== null && strpos($file, $folder) === 0) { + if ($folder !== null && str_starts_with($file, $folder)) { $file = substr($file, strlen($folder . '/')); } // Is not namespaced? Try the application folder. - if (strpos($file, '\\') === false) { + if (! str_contains($file, '\\')) { return $this->legacyLocate($file, $folder); } @@ -101,7 +103,7 @@ public function locateFile(string $file, ?string $folder = null, string $ext = ' // If we have a folder name, then the calling function // expects this file to be within that folder, like 'Views', // or 'libraries'. - if ($folder !== null && strpos($path . $filename, '/' . $folder . '/') === false) { + if ($folder !== null && ! str_contains($path . $filename, '/' . $folder . '/')) { $path .= trim($folder, '/') . '/'; } @@ -173,6 +175,8 @@ public function getClassname(string $file): string * 'app/Modules/foo/Config/Routes.php', * 'app/Modules/bar/Config/Routes.php', * ] + * + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array { @@ -188,7 +192,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp = if ($prioritizeApp) { $foundPaths[] = $fullPath; - } elseif (strpos($fullPath, APPPATH) === 0) { + } elseif (str_starts_with($fullPath, APPPATH)) { $appPaths[] = $fullPath; } else { $foundPaths[] = $fullPath; @@ -201,7 +205,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp = } // Remove any duplicates - return array_unique($foundPaths); + return array_values(array_unique($foundPaths)); } /** @@ -212,7 +216,7 @@ protected function ensureExt(string $path, string $ext): string if ($ext !== '') { $ext = '.' . $ext; - if (substr($path, -strlen($ext)) !== $ext) { + if (! str_ends_with($path, $ext)) { $path .= $ext; } } @@ -235,7 +239,7 @@ protected function getNamespaces() foreach ($this->autoloader->getNamespace() as $prefix => $paths) { foreach ($paths as $path) { if ($prefix === 'CodeIgniter') { - $system = [ + $system[] = [ 'prefix' => $prefix, 'path' => rtrim($path, '\\/') . DIRECTORY_SEPARATOR, ]; @@ -250,9 +254,7 @@ protected function getNamespaces() } } - $namespaces[] = $system; - - return $namespaces; + return array_merge($namespaces, $system); } /** @@ -277,12 +279,15 @@ public function findQualifiedNameFromPath(string $path) } if (mb_strpos($path, $namespace['path']) === 0) { - $className = '\\' . $namespace['prefix'] . '\\' . - ltrim(str_replace( + $className = $namespace['prefix'] . '\\' . + ltrim( + str_replace( '/', '\\', mb_substr($path, mb_strlen($namespace['path'])) - ), '\\'); + ), + '\\' + ); // Remove the file extension (.php) $className = mb_substr($className, 0, -4); diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php new file mode 100644 index 000000000000..adf453308de3 --- /dev/null +++ b/system/Autoloader/FileLocatorCached.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Autoloader; + +use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; + +/** + * FileLocator with Cache + * + * @see \CodeIgniter\Autoloader\FileLocatorCachedTest + */ +final class FileLocatorCached implements FileLocatorInterface +{ + /** + * @var CacheInterface|FileVarExportHandler + */ + private $cacheHandler; + + /** + * Cache data + * + * [method => data] + * E.g., + * [ + * 'search' => [$path => $foundPaths], + * ] + */ + private array $cache = []; + + /** + * Is the cache updated? + */ + private bool $cacheUpdated = false; + + private string $cacheKey = 'FileLocatorCache'; + + /** + * @param CacheInterface|FileVarExportHandler|null $cache + */ + public function __construct(private readonly FileLocator $locator, $cache = null) + { + $this->cacheHandler = $cache ?? new FileVarExportHandler(); + + $this->loadCache(); + } + + private function loadCache(): void + { + $data = $this->cacheHandler->get($this->cacheKey); + + if (is_array($data)) { + $this->cache = $data; + } + } + + public function __destruct() + { + $this->saveCache(); + } + + private function saveCache(): void + { + if ($this->cacheUpdated) { + $this->cacheHandler->save($this->cacheKey, $this->cache, 3600 * 24); + } + } + + /** + * Delete cache data + */ + public function deleteCache(): void + { + $this->cacheUpdated = false; + $this->cacheHandler->delete($this->cacheKey); + } + + public function findQualifiedNameFromPath(string $path): false|string + { + if (isset($this->cache['findQualifiedNameFromPath'][$path])) { + return $this->cache['findQualifiedNameFromPath'][$path]; + } + + $classname = $this->locator->findQualifiedNameFromPath($path); + + $this->cache['findQualifiedNameFromPath'][$path] = $classname; + $this->cacheUpdated = true; + + return $classname; + } + + public function getClassname(string $file): string + { + if (isset($this->cache['getClassname'][$file])) { + return $this->cache['getClassname'][$file]; + } + + $classname = $this->locator->getClassname($file); + + $this->cache['getClassname'][$file] = $classname; + $this->cacheUpdated = true; + + return $classname; + } + + public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array + { + if (isset($this->cache['search'][$path][$ext][$prioritizeApp])) { + return $this->cache['search'][$path][$ext][$prioritizeApp]; + } + + $foundPaths = $this->locator->search($path, $ext, $prioritizeApp); + + $this->cache['search'][$path][$ext][$prioritizeApp] = $foundPaths; + $this->cacheUpdated = true; + + return $foundPaths; + } + + public function listFiles(string $path): array + { + if (isset($this->cache['listFiles'][$path])) { + return $this->cache['listFiles'][$path]; + } + + $files = $this->locator->listFiles($path); + + $this->cache['listFiles'][$path] = $files; + $this->cacheUpdated = true; + + return $files; + } + + public function listNamespaceFiles(string $prefix, string $path): array + { + if (isset($this->cache['listNamespaceFiles'][$prefix][$path])) { + return $this->cache['listNamespaceFiles'][$prefix][$path]; + } + + $files = $this->locator->listNamespaceFiles($prefix, $path); + + $this->cache['listNamespaceFiles'][$prefix][$path] = $files; + $this->cacheUpdated = true; + + return $files; + } + + public function locateFile(string $file, ?string $folder = null, string $ext = 'php'): false|string + { + if (isset($this->cache['locateFile'][$file][$folder][$ext])) { + return $this->cache['locateFile'][$file][$folder][$ext]; + } + + $files = $this->locator->locateFile($file, $folder, $ext); + + $this->cache['locateFile'][$file][$folder][$ext] = $files; + $this->cacheUpdated = true; + + return $files; + } +} diff --git a/system/Autoloader/FileLocatorInterface.php b/system/Autoloader/FileLocatorInterface.php new file mode 100644 index 000000000000..3f7355a8f09d --- /dev/null +++ b/system/Autoloader/FileLocatorInterface.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Autoloader; + +/** + * Allows loading non-class files in a namespaced manner. + * Works with Helpers, Views, etc. + */ +interface FileLocatorInterface +{ + /** + * Attempts to locate a file by examining the name for a namespace + * and looking through the PSR-4 namespaced files that we know about. + * + * @param string $file The relative file path or namespaced file to + * locate. If not namespaced, search in the app + * folder. + * @param non-empty-string|null $folder The folder within the namespace that we should + * look for the file. If $file does not contain + * this value, it will be appended to the namespace + * folder. + * @param string $ext The file extension the file should have. + * + * @return false|string The path to the file, or false if not found. + */ + public function locateFile(string $file, ?string $folder = null, string $ext = 'php'); + + /** + * Examines a file and returns the fully qualified class name. + */ + public function getClassname(string $file): string; + + /** + * Searches through all of the defined namespaces looking for a file. + * Returns an array of all found locations for the defined file. + * + * Example: + * + * $locator->search('Config/Routes.php'); + * // Assuming PSR4 namespaces include foo and bar, might return: + * [ + * 'app/Modules/foo/Config/Routes.php', + * 'app/Modules/bar/Config/Routes.php', + * ] + */ + public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array; + + /** + * Find the qualified name of a file according to + * the namespace of the first matched namespace path. + * + * @return false|string The qualified name or false if the path is not found + */ + public function findQualifiedNameFromPath(string $path); + + /** + * Scans the defined namespaces, returning a list of all files + * that are contained within the subpath specified by $path. + * + * @return list List of file paths + */ + public function listFiles(string $path): array; + + /** + * Scans the provided namespace, returning a list of all files + * that are contained within the sub path specified by $path. + * + * @return list List of file paths + */ + public function listNamespaceFiles(string $prefix, string $path): array; +} diff --git a/system/BaseModel.php b/system/BaseModel.php index cd2bdd6beb22..5d08ec59e04c 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1,5 +1,7 @@ [column => type] + */ + protected array $casts = []; + + /** + * Custom convert handlers. + * + * @var array [type => classname] + */ + protected array $castHandlers = []; + + protected ?DataConverter $converter = null; + /** * If this model should use "softDeletes" and * simply set a date when rows are deleted, or @@ -92,7 +128,7 @@ abstract class BaseModel * * @var bool */ - protected $useSoftDeletes = false; + protected $protectFields = true; /** * An array of field names that are allowed @@ -134,6 +170,15 @@ abstract class BaseModel */ protected $updatedField = 'updated_at'; + /** + * If this model should use "softDeletes" and + * simply set a date when rows are deleted, or + * do hard deletes. + * + * @var bool + */ + protected $useSoftDeletes = false; + /** * Used by withDeleted to override the * model's softDelete setting. @@ -150,27 +195,14 @@ abstract class BaseModel protected $deletedField = 'deleted_at'; /** - * Used by asArray and asObject to provide - * temporary overrides of model default. - * - * @var string - */ - protected $tempReturnType; - - /** - * Whether we should limit fields in inserts - * and updates to those available in $allowedFields or not. - * - * @var bool + * Whether to allow inserting empty data. */ - protected $protectFields = true; + protected bool $allowEmptyInserts = false; /** - * Database Connection - * - * @var BaseConnection + * Whether to update Entity's only changed data. */ - protected $db; + protected bool $updateOnlyChanged = true; /** * Rules used to validate data in insert(), update(), and save() methods. @@ -211,7 +243,7 @@ abstract class BaseModel /** * Our validator instance. * - * @var ValidationInterface + * @var ValidationInterface|null */ protected $validation; @@ -327,24 +359,38 @@ abstract class BaseModel */ protected $afterDelete = []; - /** - * Whether to allow inserting empty data. - */ - protected bool $allowEmptyInserts = false; - public function __construct(?ValidationInterface $validation = null) { $this->tempReturnType = $this->returnType; $this->tempUseSoftDeletes = $this->useSoftDeletes; $this->tempAllowCallbacks = $this->allowCallbacks; - /** - * @var ValidationInterface|null $validation - */ - $validation ??= Services::validation(null, false); $this->validation = $validation; $this->initialize(); + $this->createDataConverter(); + } + + /** + * Creates DataConverter instance. + */ + protected function createDataConverter(): void + { + if ($this->useCasts()) { + $this->converter = new DataConverter( + $this->casts, + $this->castHandlers, + $this->db + ); + } + } + + /** + * Are casts used? + */ + protected function useCasts(): bool + { + return $this->casts !== []; } /** @@ -384,12 +430,12 @@ abstract protected function doFindColumn(string $columnName); * Fetches all results, while optionally limiting them. * This method works only with dbCalls. * - * @param int $limit Limit - * @param int $offset Offset + * @param int|null $limit Limit + * @param int $offset Offset * * @return array */ - abstract protected function doFindAll(int $limit = 0, int $offset = 0); + abstract protected function doFindAll(?int $limit = null, int $offset = 0); /** * Returns the first row of the result set. @@ -499,17 +545,6 @@ abstract protected function doReplace(?array $row = null, bool $returnSQL = fals */ abstract protected function doErrors(); - /** - * Returns the id value for the data array or object. - * - * @param array|object $data Data - * - * @return array|int|string|null - * - * @deprecated Add an override on getIdValue() instead. Will be removed in version 5.0. - */ - abstract protected function idValue($data); - /** * Public getter to return the id value using the idValue() method. * For example with SQL this will return $data->$this->primaryKey. @@ -518,13 +553,8 @@ abstract protected function idValue($data); * @phpstan-param row_array|object $row * * @return array|int|string|null - * - * @todo: Make abstract in version 5.0 */ - public function getIdValue($row) - { - return $this->idValue($row); - } + abstract public function getIdValue($row); /** * Override countAllResults to account for soft deleted accounts. @@ -604,7 +634,7 @@ public function find($id = null) */ public function findColumn(string $columnName) { - if (strpos($columnName, ',') !== false) { + if (str_contains($columnName, ',')) { throw DataException::forFindColumnHaveMultipleColumns(); } @@ -621,8 +651,13 @@ public function findColumn(string $columnName) * * @return array */ - public function findAll(int $limit = 0, int $offset = 0) + public function findAll(?int $limit = null, int $offset = 0) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll) { + $limit ??= 0; + } + if ($this->tempAllowCallbacks) { // Call the before event and check for a return $eventData = $this->trigger('beforeFind', [ @@ -1208,6 +1243,10 @@ public function replace(?array $row = null, bool $returnSQL = false) */ public function errors(bool $forceDB = false) { + if ($this->validation === null) { + return $this->doErrors(); + } + // Do we have validation errors? if (! $forceDB && ! $this->skipValidation && ($errors = $this->validation->getErrors())) { return $errors; @@ -1231,7 +1270,7 @@ public function errors(bool $forceDB = false) public function paginate(?int $perPage = null, string $group = 'default', ?int $page = null, int $segment = 0) { // Since multiple models may use the Pager, the Pager must be shared. - $pager = Services::pager(); + $pager = service('pager'); if ($segment !== 0) { $pager->setSegment($segment, $group); @@ -1358,19 +1397,12 @@ protected function setDate(?int $userData = null) */ protected function intToDate(int $value) { - switch ($this->dateFormat) { - case 'int': - return $value; - - case 'datetime': - return date('Y-m-d H:i:s', $value); - - case 'date': - return date('Y-m-d', $value); - - default: - throw ModelException::forNoDateFormat(static::class); - } + return match ($this->dateFormat) { + 'int' => $value, + 'datetime' => date($this->db->dateFormat['datetime'], $value), + 'date' => date($this->db->dateFormat['date'], $value), + default => throw ModelException::forNoDateFormat(static::class), + }; } /** @@ -1387,19 +1419,12 @@ protected function intToDate(int $value) */ protected function timeToDate(Time $value) { - switch ($this->dateFormat) { - case 'datetime': - return $value->format('Y-m-d H:i:s'); - - case 'date': - return $value->format('Y-m-d'); - - case 'int': - return $value->getTimestamp(); - - default: - return (string) $value; - } + return match ($this->dateFormat) { + 'datetime' => $value->format($this->db->dateFormat['datetime']), + 'date' => $value->format($this->db->dateFormat['date']), + 'int' => $value->getTimestamp(), + default => (string) $value, + }; } /** @@ -1478,6 +1503,8 @@ public function setValidationRule(string $field, $fieldRules) // ValidationRules can be either a string, which is the group name, // or an array of rules. if (is_string($rules)) { + $this->ensureValidation(); + [$rules, $customErrors] = $this->validation->loadRuleGroup($rules); $this->validationRules = $rules; @@ -1513,14 +1540,22 @@ public function cleanRules(bool $choice = false) */ public function validate($row): bool { + if ($this->skipValidation) { + return true; + } + $rules = $this->getValidationRules(); + if ($rules === []) { + return true; + } + // Validation requires array, so cast away. if (is_object($row)) { $row = (array) $row; } - if ($this->skipValidation || $rules === [] || $row === []) { + if ($row === []) { return true; } @@ -1532,6 +1567,8 @@ public function validate($row): bool return true; } + $this->ensureValidation(); + $this->validation->reset()->setRules($rules, $this->validationMessages); return $this->validation->run($row, null, $this->DBGroup); @@ -1550,6 +1587,8 @@ public function getValidationRules(array $options = []): array // ValidationRules can be either a string, which is the group name, // or an array of rules. if (is_string($rules)) { + $this->ensureValidation(); + [$rules, $customErrors] = $this->validation->loadRuleGroup($rules); $this->validationMessages += $customErrors; @@ -1564,6 +1603,13 @@ public function getValidationRules(array $options = []): array return $rules; } + protected function ensureValidation(): void + { + if ($this->validation === null) { + $this->validation = Services::validation(null, false); + } + } + /** * Returns the model's validation messages, so they * can be used elsewhere, if needed. @@ -1670,7 +1716,7 @@ public function asArray() * class vars with the same name as the collection columns, * or at least allows them to be created. * - * @param string $class Class Name + * @param 'object'|class-string $class Class Name * * @return $this */ @@ -1733,7 +1779,7 @@ protected function timeToString(array $properties): array * @param bool $onlyChanged Only Changed Property * @param bool $recursive If true, inner entities will be casted as array as well * - * @return array + * @return array Array with raw values. * * @throws ReflectionException */ @@ -1770,6 +1816,9 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec * @throws DataException * @throws InvalidArgumentException * @throws ReflectionException + * + * @used-by insert() + * @used-by update() */ protected function transformDataToArray($row, string $type): array { @@ -1781,14 +1830,31 @@ protected function transformDataToArray($row, string $type): array throw DataException::forEmptyDataset($type); } + // If it validates with entire rules, all fields are needed. + if ($this->skipValidation === false && $this->cleanValidationRules === false) { + $onlyChanged = false; + } else { + $onlyChanged = ($type === 'update' && $this->updateOnlyChanged); + } + + if ($this->useCasts()) { + if (is_array($row)) { + $row = $this->converter->toDataSource($row); + } elseif ($row instanceof stdClass) { + $row = (array) $row; + $row = $this->converter->toDataSource($row); + } elseif ($row instanceof Entity) { + $row = $this->converter->extract($row, $onlyChanged); + // Convert any Time instances to appropriate $dateFormat + $row = $this->timeToString($row); + } elseif (is_object($row)) { + $row = $this->converter->extract($row, $onlyChanged); + } + } // If $row is using a custom class with public or protected // properties representing the collection elements, we need to grab // them as an array. - if (is_object($row) && ! $row instanceof stdClass) { - // If it validates with entire rules, all fields are needed. - $onlyChanged = ($this->skipValidation === false && $this->cleanValidationRules === false) - ? false : ($type === 'update'); - + elseif (is_object($row) && ! $row instanceof stdClass) { $row = $this->objectToArray($row, $onlyChanged, true); } @@ -1855,65 +1921,31 @@ public function __call(string $name, array $params) } /** - * Replace any placeholders within the rules with the values that - * match the 'key' of any properties being set. For example, if - * we had the following $data array: - * - * [ 'id' => 13 ] - * - * and the following rule: - * - * 'required|is_unique[users,email,id,{id}]' - * - * The value of {id} would be replaced with the actual id in the form data: - * - * 'required|is_unique[users,email,id,13]' - * - * @param array $rules Validation rules - * @param array $data Data - * - * @codeCoverageIgnore - * - * @deprecated use fillPlaceholders($rules, $data) from Validation instead + * Sets $allowEmptyInserts. */ - protected function fillPlaceholders(array $rules, array $data): array + public function allowEmptyInserts(bool $value = true): self { - $replacements = []; - - foreach ($data as $key => $value) { - $replacements['{' . $key . '}'] = $value; - } - - if ($replacements !== []) { - foreach ($rules as &$rule) { - if (is_array($rule)) { - foreach ($rule as &$row) { - // Should only be an `errors` array - // which doesn't take placeholders. - if (is_array($row)) { - continue; - } - - $row = strtr($row, $replacements); - } - - continue; - } - - $rule = strtr($rule, $replacements); - } - } + $this->allowEmptyInserts = $value; - return $rules; + return $this; } /** - * Sets $allowEmptyInserts. + * Converts database data array to return type value. + * + * @param array $row Raw data from database + * @param 'array'|'object'|class-string $returnType */ - public function allowEmptyInserts(bool $value = true): self + protected function convertToReturnType(array $row, string $returnType): array|object { - $this->allowEmptyInserts = $value; + if ($returnType === 'array') { + return $this->converter->fromDataSource($row); + } - return $this; + if ($returnType === 'object') { + return (object) $this->converter->fromDataSource($row); + } + + return $this->converter->reconstruct($returnType, $row); } } diff --git a/system/Boot.php b/system/Boot.php new file mode 100644 index 000000000000..8a769bbb6866 --- /dev/null +++ b/system/Boot.php @@ -0,0 +1,342 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter; + +use CodeIgniter\Cache\FactoriesCache; +use CodeIgniter\CLI\Console; +use CodeIgniter\Config\DotEnv; +use Config\Autoload; +use Config\Modules; +use Config\Optimize; +use Config\Paths; +use Config\Services; + +/** + * Bootstrap for the application + * + * @codeCoverageIgnore + */ +class Boot +{ + /** + * Used by `public/index.php` + * + * Context + * web: Invoked by HTTP request + * php-cli: Invoked by CLI via `php public/index.php` + * + * @return int Exit code. + */ + public static function bootWeb(Paths $paths): int + { + static::definePathConstants($paths); + if (! defined('APP_NAMESPACE')) { + static::loadConstants(); + } + static::checkMissingExtensions(); + + static::loadDotEnv($paths); + static::defineEnvironment(); + static::loadEnvironmentBootstrap($paths); + + static::loadCommonFunctions(); + static::loadAutoloader(); + static::setExceptionHandler(); + static::initializeKint(); + + $configCacheEnabled = class_exists(Optimize::class) + && (new Optimize())->configCacheEnabled; + if ($configCacheEnabled) { + $factoriesCache = static::loadConfigCache(); + } + + static::autoloadHelpers(); + + $app = static::initializeCodeIgniter(); + static::runCodeIgniter($app); + + if ($configCacheEnabled) { + static::saveConfigCache($factoriesCache); + } + + // Exits the application, setting the exit code for CLI-based + // applications that might be watching. + return EXIT_SUCCESS; + } + + /** + * Used by `spark` + * + * @return int Exit code. + */ + public static function bootSpark(Paths $paths): int + { + static::definePathConstants($paths); + if (! defined('APP_NAMESPACE')) { + static::loadConstants(); + } + static::checkMissingExtensions(); + + static::loadDotEnv($paths); + static::defineEnvironment(); + static::loadEnvironmentBootstrap($paths); + + static::loadCommonFunctions(); + static::loadAutoloader(); + static::setExceptionHandler(); + static::initializeKint(); + static::autoloadHelpers(); + + static::initializeCodeIgniter(); + $console = static::initializeConsole(); + + return static::runCommand($console); + } + + /** + * Used by `system/Test/bootstrap.php` + */ + public static function bootTest(Paths $paths): void + { + static::loadConstants(); + static::checkMissingExtensions(); + + static::loadDotEnv($paths); + static::loadEnvironmentBootstrap($paths, false); + + static::loadCommonFunctions(); + static::loadAutoloader(); + static::setExceptionHandler(); + static::initializeKint(); + static::autoloadHelpers(); + } + + /** + * Load environment settings from .env files into $_SERVER and $_ENV + */ + protected static function loadDotEnv(Paths $paths): void + { + require_once $paths->systemDirectory . '/Config/DotEnv.php'; + (new DotEnv($paths->appDirectory . '/../'))->load(); + } + + protected static function defineEnvironment(): void + { + if (! defined('ENVIRONMENT')) { + // @phpstan-ignore-next-line + $env = $_ENV['CI_ENVIRONMENT'] ?? $_SERVER['CI_ENVIRONMENT'] + ?? getenv('CI_ENVIRONMENT') + ?: 'production'; + + define('ENVIRONMENT', $env); + } + } + + protected static function loadEnvironmentBootstrap(Paths $paths, bool $exit = true): void + { + if (is_file($paths->appDirectory . '/Config/Boot/' . ENVIRONMENT . '.php')) { + require_once $paths->appDirectory . '/Config/Boot/' . ENVIRONMENT . '.php'; + + return; + } + + if ($exit) { + header('HTTP/1.1 503 Service Unavailable.', true, 503); + echo 'The application environment is not set correctly.'; + + exit(EXIT_ERROR); + } + } + + /** + * The path constants provide convenient access to the folders throughout + * the application. We have to set them up here, so they are available in + * the config files that are loaded. + */ + protected static function definePathConstants(Paths $paths): void + { + // The path to the application directory. + if (! defined('APPPATH')) { + define('APPPATH', realpath(rtrim($paths->appDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); + } + + // The path to the project root directory. Just above APPPATH. + if (! defined('ROOTPATH')) { + define('ROOTPATH', realpath(APPPATH . '../') . DIRECTORY_SEPARATOR); + } + + // The path to the system directory. + if (! defined('SYSTEMPATH')) { + define('SYSTEMPATH', realpath(rtrim($paths->systemDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); + } + + // The path to the writable directory. + if (! defined('WRITEPATH')) { + define('WRITEPATH', realpath(rtrim($paths->writableDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); + } + + // The path to the tests directory + if (! defined('TESTPATH')) { + define('TESTPATH', realpath(rtrim($paths->testsDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); + } + } + + protected static function loadConstants(): void + { + require_once APPPATH . 'Config/Constants.php'; + } + + protected static function loadCommonFunctions(): void + { + // Require app/Common.php file if exists. + if (is_file(APPPATH . 'Common.php')) { + require_once APPPATH . 'Common.php'; + } + + // Require system/Common.php + require_once SYSTEMPATH . 'Common.php'; + } + + /** + * The autoloader allows all the pieces to work together in the framework. + * We have to load it here, though, so that the config files can use the + * path constants. + */ + protected static function loadAutoloader(): void + { + if (! class_exists(Autoload::class, false)) { + require_once SYSTEMPATH . 'Config/AutoloadConfig.php'; + require_once APPPATH . 'Config/Autoload.php'; + require_once SYSTEMPATH . 'Modules/Modules.php'; + require_once APPPATH . 'Config/Modules.php'; + } + + require_once SYSTEMPATH . 'Autoloader/Autoloader.php'; + require_once SYSTEMPATH . 'Config/BaseService.php'; + require_once SYSTEMPATH . 'Config/Services.php'; + require_once APPPATH . 'Config/Services.php'; + + // Initialize and register the loader with the SPL autoloader stack. + Services::autoloader()->initialize(new Autoload(), new Modules())->register(); + } + + protected static function autoloadHelpers(): void + { + Services::autoloader()->loadHelpers(); + } + + protected static function setExceptionHandler(): void + { + Services::exceptions()->initialize(); + } + + protected static function checkMissingExtensions(): void + { + if (is_file(COMPOSER_PATH)) { + return; + } + + // Run this check for manual installations + $missingExtensions = []; + + foreach ([ + 'intl', + 'json', + 'mbstring', + ] as $extension) { + if (! extension_loaded($extension)) { + $missingExtensions[] = $extension; + } + } + + if ($missingExtensions === []) { + return; + } + + $message = sprintf( + 'The framework needs the following extension(s) installed and loaded: %s.', + implode(', ', $missingExtensions) + ); + + header('HTTP/1.1 503 Service Unavailable.', true, 503); + echo $message; + + exit(EXIT_ERROR); + } + + protected static function initializeKint(): void + { + Services::autoloader()->initializeKint(CI_DEBUG); + } + + protected static function loadConfigCache(): FactoriesCache + { + $factoriesCache = new FactoriesCache(); + $factoriesCache->load('config'); + + return $factoriesCache; + } + + /** + * The CodeIgniter class contains the core functionality to make + * the application run, and does all the dirty work to get + * the pieces all working together. + */ + protected static function initializeCodeIgniter(): CodeIgniter + { + $app = Config\Services::codeigniter(); + $app->initialize(); + $context = is_cli() ? 'php-cli' : 'web'; + $app->setContext($context); + + return $app; + } + + /** + * Now that everything is set up, it's time to actually fire + * up the engines and make this app do its thang. + */ + protected static function runCodeIgniter(CodeIgniter $app): void + { + $app->run(); + } + + protected static function saveConfigCache(FactoriesCache $factoriesCache): void + { + $factoriesCache->save('config'); + } + + protected static function initializeConsole(): Console + { + $console = new Console(); + + // Show basic information before we do anything else. + // @phpstan-ignore-next-line + if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { + unset($_SERVER['argv'][$suppress]); // @phpstan-ignore-line + $suppress = true; + } + + $console->showHeader($suppress); + + return $console; + } + + protected static function runCommand(Console $console): int + { + $exit = $console->run(); + + return is_int($exit) ? $exit : EXIT_SUCCESS; + } +} diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index 8101283b908a..1b273846bd74 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -1,5 +1,7 @@ input($prefix); } /** @@ -220,13 +215,11 @@ public static function input(?string $prefix = null): string * // Do not provide options but requires a valid email * $email = CLI::prompt('What is your email?', null, 'required|valid_email'); * - * @param string $field Output "field" question - * @param array|string $options String to a default value, array to a list of options (the first option will be the default value) - * @param array|string|null $validation Validation rules + * @param string $field Output "field" question + * @param list|string $options String to a default value, array to a list of options (the first option will be the default value) + * @param array|string|null $validation Validation rules * * @return string The user input - * - * @codeCoverageIgnore */ public static function prompt(string $field, $options = null, $validation = null): string { @@ -246,9 +239,9 @@ public static function prompt(string $field, $options = null, $validation = null $default = $options; } - if (is_array($options) && $options) { + if (is_array($options) && $options !== []) { $opts = $options; - $extraOutputDefault = static::color($opts[0], 'green'); + $extraOutputDefault = static::color((string) $opts[0], 'green'); unset($opts[0]); @@ -265,7 +258,7 @@ public static function prompt(string $field, $options = null, $validation = null static::fwrite(STDOUT, $field . (trim($field) !== '' ? ' ' : '') . $extraOutput . ': '); // Read the input from keyboard. - $input = trim(static::input()) ?: $default; + $input = trim(static::$io->input()) ?: (string) $default; if ($validation !== []) { while (! static::validate('"' . trim($field) . '"', $input, $validation)) { @@ -285,8 +278,6 @@ public static function prompt(string $field, $options = null, $validation = null * @param array|string|null $validation Validation rules * * @return string The selected key of $options - * - * @codeCoverageIgnore */ public static function promptByKey($text, array $options, $validation = null): string { @@ -415,8 +406,6 @@ private static function printKeysAndValues(array $options): void * @param string $field Prompt "field" output * @param string $value Input value * @param array|string $rules Validation rules - * - * @codeCoverageIgnore */ protected static function validate(string $field, string $value, $rules): bool { @@ -533,11 +522,8 @@ public static function wait(int $seconds, bool $countdown = false) } elseif ($seconds > 0) { sleep($seconds); } else { - // this chunk cannot be tested because of keyboard input - // @codeCoverageIgnoreStart static::write(static::$wait_msg); - static::input(); - // @codeCoverageIgnoreEnd + static::$io->input(); } } @@ -567,8 +553,6 @@ public static function newLine(int $num = 1) /** * Clears the screen of output * - * @codeCoverageIgnore - * * @return void */ public static function clearScreen() @@ -608,7 +592,7 @@ public static function color(string $text, string $foreground, ?string $backgrou $newText = ''; // Detect if color method was already in use with this text - if (strpos($text, "\033[0m") !== false) { + if (str_contains($text, "\033[0m")) { $pattern = '/\\033\\[0;.+?\\033\\[0m/u'; preg_match_all($pattern, $text, $matches); @@ -762,8 +746,6 @@ public static function getHeight(int $default = 32): int /** * Populates the CLI's dimensions. * - * @codeCoverageIgnore - * * @return void */ public static function generateDimensions() @@ -1066,7 +1048,7 @@ public static function table(array $tbody, array $thead = []) foreach ($tableRows[$row] as $col) { // Sets the size of this column in the current row - $allColsLengths[$row][$column] = static::strlen($col); + $allColsLengths[$row][$column] = static::strlen((string) $col); // If the current column does not have a value among the larger ones // or the value of this is greater than the existing one @@ -1086,7 +1068,7 @@ public static function table(array $tbody, array $thead = []) $column = 0; foreach ($tableRows[$row] as $col) { - $diff = $maxColsLengths[$column] - static::strlen($col); + $diff = $maxColsLengths[$column] - static::strlen((string) $col); if ($diff !== 0) { $tableRows[$row][$column] .= str_repeat(' ', $diff); @@ -1106,7 +1088,7 @@ public static function table(array $tbody, array $thead = []) $cols = '+'; foreach ($tableRows[$row] as $col) { - $cols .= str_repeat('-', static::strlen($col) + 2) . '+'; + $cols .= str_repeat('-', static::strlen((string) $col) + 2) . '+'; } $table .= $cols . PHP_EOL; } @@ -1137,15 +1119,27 @@ public static function table(array $tbody, array $thead = []) */ protected static function fwrite($handle, string $string) { - if (! is_cli()) { - // @codeCoverageIgnoreStart - echo $string; + static::$io->fwrite($handle, $string); + } - return; - // @codeCoverageIgnoreEnd - } + /** + * Testing purpose only + * + * @testTag + */ + public static function setInputOutput(InputOutput $io): void + { + static::$io = $io; + } - fwrite($handle, $string); + /** + * Testing purpose only + * + * @testTag + */ + public static function resetInputOutput(): void + { + static::$io = new InputOutput(); } } diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index 1de0df2791d6..30bd2c2652cd 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -1,5 +1,7 @@ commands[$command]['class']; $class = new $className($this->logger, $this); - return $class->run($params); + Events::trigger('pre_command'); + + $exit = $class->run($params); + + Events::trigger('post_command'); + + return $exit; } /** @@ -87,7 +96,7 @@ public function discoverCommands() return; } - /** @var FileLocator $locator */ + /** @var FileLocatorInterface $locator */ $locator = service('locator'); $files = $locator->listFiles('Commands/'); @@ -100,9 +109,9 @@ public function discoverCommands() // Loop over each file checking to see if a command with that // alias exists in the class. foreach ($files as $file) { - $className = $locator->getClassname($file); + $className = $locator->findQualifiedNameFromPath($file); - if ($className === '' || ! class_exists($className)) { + if ($className === false || ! class_exists($className)) { continue; } @@ -174,7 +183,7 @@ protected function getCommandAlternatives(string $name, array $collection): arra foreach (array_keys($collection) as $commandName) { $lev = levenshtein($name, $commandName); - if ($lev <= strlen($commandName) / 3 || strpos($commandName, $name) !== false) { + if ($lev <= strlen($commandName) / 3 || str_contains($commandName, $name)) { $alternatives[$commandName] = $lev; } } diff --git a/system/CLI/Console.php b/system/CLI/Console.php index adcf9aaeca8b..725193d424b5 100644 --- a/system/CLI/Console.php +++ b/system/CLI/Console.php @@ -1,5 +1,7 @@ params = $params; @@ -109,7 +124,7 @@ protected function generateClass(array $params) $target = $this->buildPath($class); // Check if path is empty. - if (empty($target)) { + if ($target === '') { return; } @@ -118,15 +133,17 @@ protected function generateClass(array $params) /** * Generate a view file from an existing template. + * + * @param string $view namespaced view name that is generated */ - protected function generateView(string $view, array $params) + protected function generateView(string $view, array $params): void { $this->params = $params; $target = $this->buildPath($view); // Check if path is empty. - if (empty($target)) { + if ($target === '') { return; } @@ -135,6 +152,8 @@ protected function generateView(string $view, array $params) /** * Handles writing the file to disk, and all of the safety checks around that. + * + * @param string $target file path */ private function generateFile(string $target, string $content): void { @@ -143,7 +162,13 @@ private function generateFile(string $target, string $content): void CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow'); CLI::newLine(); - if (CLI::prompt('Are you sure you want to continue?', ['y', 'n'], 'required') === 'n') { + if ( + CLI::prompt( + 'Are you sure you want to continue?', + ['y', 'n'], + 'required' + ) === 'n' + ) { CLI::newLine(); CLI::write(lang('CLI.generator.cancelOperation'), 'yellow'); CLI::newLine(); @@ -160,7 +185,11 @@ private function generateFile(string $target, string $content): void // Overwriting files unknowingly is a serious annoyance, So we'll check if // we are duplicating things, If 'force' option is not supplied, we bail. if (! $this->getOption('force') && $isFile) { - CLI::error(lang('CLI.generator.fileExist', [clean_path($target)]), 'light_gray', 'red'); + CLI::error( + lang('CLI.generator.fileExist', [clean_path($target)]), + 'light_gray', + 'red' + ); CLI::newLine(); return; @@ -179,7 +208,11 @@ private function generateFile(string $target, string $content): void // contents from the template, and then we'll do the necessary replacements. if (! write_file($target, $content)) { // @codeCoverageIgnoreStart - CLI::error(lang('CLI.generator.fileError', [clean_path($target)]), 'light_gray', 'red'); + CLI::error( + lang('CLI.generator.fileError', [clean_path($target)]), + 'light_gray', + 'red' + ); CLI::newLine(); return; @@ -187,18 +220,28 @@ private function generateFile(string $target, string $content): void } if ($this->getOption('force') && $isFile) { - CLI::write(lang('CLI.generator.fileOverwrite', [clean_path($target)]), 'yellow'); + CLI::write( + lang('CLI.generator.fileOverwrite', [clean_path($target)]), + 'yellow' + ); CLI::newLine(); return; } - CLI::write(lang('CLI.generator.fileCreate', [clean_path($target)]), 'green'); + CLI::write( + lang('CLI.generator.fileCreate', [clean_path($target)]), + 'green' + ); CLI::newLine(); } /** * Prepare options and do the necessary replacements. + * + * @param string $class namespaced classname or namespaced view. + * + * @return string generated file content */ protected function prepare(string $class): string { @@ -219,14 +262,35 @@ protected function basename(string $filename): string * Parses the class name and checks if it is already qualified. */ protected function qualifyClassName(): string + { + $class = $this->normalizeInputClassName(); + + // Gets the namespace from input. Don't forget the ending backslash! + $namespace = $this->getNamespace() . '\\'; + + if (str_starts_with($class, $namespace)) { + return $class; // @codeCoverageIgnore + } + + $directoryString = ($this->directory !== null) ? $this->directory . '\\' : ''; + + return $namespace . $directoryString . str_replace('/', '\\', $class); + } + + /** + * Normalize input classname. + */ + private function normalizeInputClassName(): string { // Gets the class name from input. $class = $this->params[0] ?? CLI::getSegment(2); if ($class === null && $this->hasClassName) { // @codeCoverageIgnoreStart - $nameLang = $this->classNameLang ?: 'CLI.generator.className.default'; - $class = CLI::prompt(lang($nameLang), null, 'required'); + $nameLang = $this->classNameLang !== '' + ? $this->classNameLang + : 'CLI.generator.className.default'; + $class = CLI::prompt(lang($nameLang), null, 'required'); CLI::newLine(); // @codeCoverageIgnoreEnd } @@ -244,21 +308,24 @@ protected function qualifyClassName(): string $class = $matches[1] . ucfirst($matches[2]); } - if ($this->enabledSuffixing && $this->getOption('suffix') && preg_match($pattern, $class) !== 1) { + if ( + $this->enabledSuffixing && $this->getOption('suffix') + && preg_match($pattern, $class) !== 1 + ) { $class .= ucfirst($component); } // Trims input, normalize separators, and ensure that all paths are in Pascalcase. - $class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/'); - - // Gets the namespace from input. Don't forget the ending backslash! - $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\') . '\\'; - - if (strncmp($class, $namespace, strlen($namespace)) === 0) { - return $class; // @codeCoverageIgnore - } - - return $namespace . $this->directory . '\\' . str_replace('/', '\\', $class); + return ltrim( + implode( + '\\', + array_map( + 'pascalize', + explode('\\', str_replace('/', '\\', trim($class))) + ) + ), + '\\/' + ); } /** @@ -268,21 +335,41 @@ protected function qualifyClassName(): string protected function renderTemplate(array $data = []): string { try { - return view(config(Generators::class)->views[$this->name], $data, ['debug' => false]); + $template = $this->templatePath ?? config(Generators::class)->views[$this->name]; + + return view($template, $data, ['debug' => false]); } catch (Throwable $e) { log_message('error', (string) $e); - return view("CodeIgniter\\Commands\\Generators\\Views\\{$this->template}", $data, ['debug' => false]); + return view( + "CodeIgniter\\Commands\\Generators\\Views\\{$this->template}", + $data, + ['debug' => false] + ); } } /** * Performs pseudo-variables contained within view file. + * + * @param string $class namespaced classname or namespaced view. + * + * @return string generated file content */ - protected function parseTemplate(string $class, array $search = [], array $replace = [], array $data = []): string - { + protected function parseTemplate( + string $class, + array $search = [], + array $replace = [], + array $data = [] + ): string { // Retrieves the namespace part from the fully qualified class name. - $namespace = trim(implode('\\', array_slice(explode('\\', $class), 0, -1)), '\\'); + $namespace = trim( + implode( + '\\', + array_slice(explode('\\', $class), 0, -1) + ), + '\\' + ); $search[] = '<@php'; $search[] = '{namespace}'; $search[] = '{class}'; @@ -302,7 +389,14 @@ protected function buildContent(string $class): string { $template = $this->prepare($class); - if ($this->sortImports && preg_match('/(?P(?:^use [^;]+;$\n?)+)/m', $template, $match)) { + if ( + $this->sortImports + && preg_match( + '/(?P(?:^use [^;]+;$\n?)+)/m', + $template, + $match + ) + ) { $imports = explode("\n", trim($match['imports'])); sort($imports); @@ -314,25 +408,62 @@ protected function buildContent(string $class): string /** * Builds the file path from the class name. + * + * @param string $class namespaced classname or namespaced view. */ protected function buildPath(string $class): string { - $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\'); + $namespace = $this->getNamespace(); // Check if the namespace is actually defined and we are not just typing gibberish. - $base = Services::autoloader()->getNamespace($namespace); + $base = service('autoloader')->getNamespace($namespace); if (! $base = reset($base)) { - CLI::error(lang('CLI.namespaceNotDefined', [$namespace]), 'light_gray', 'red'); + CLI::error( + lang('CLI.namespaceNotDefined', [$namespace]), + 'light_gray', + 'red' + ); CLI::newLine(); return ''; } - $base = realpath($base) ?: $base; - $file = $base . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, trim(str_replace($namespace . '\\', '', $class), '\\')) . '.php'; + $realpath = realpath($base); + $base = ($realpath !== false) ? $realpath : $base; + + $file = $base . DIRECTORY_SEPARATOR + . str_replace( + '\\', + DIRECTORY_SEPARATOR, + trim(str_replace($namespace . '\\', '', $class), '\\') + ) . '.php'; + + return implode( + DIRECTORY_SEPARATOR, + array_slice( + explode(DIRECTORY_SEPARATOR, $file), + 0, + -1 + ) + ) . DIRECTORY_SEPARATOR . $this->basename($file); + } - return implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, -1)) . DIRECTORY_SEPARATOR . $this->basename($file); + /** + * Gets the namespace from the command-line option, + * or the default namespace if the option is not set. + * Can be overridden by directly setting $this->namespace. + */ + protected function getNamespace(): string + { + return $this->namespace ?? trim( + str_replace( + '/', + '\\', + $this->getOption('namespace') ?? APP_NAMESPACE + ), + '\\' + ); } /** @@ -374,10 +505,8 @@ protected function setEnabledSuffixing(bool $enabledSuffixing) /** * Gets a single command-line option. Returns TRUE if the option exists, * but doesn't have a value, and is simply acting as a flag. - * - * @return mixed */ - protected function getOption(string $name) + protected function getOption(string $name): string|bool|null { if (! array_key_exists($name, $this->params)) { return CLI::getOption($name); diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php new file mode 100644 index 000000000000..b69c19e2eee1 --- /dev/null +++ b/system/CLI/InputOutput.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI; + +/** + * Input and Output for CLI. + */ +class InputOutput +{ + /** + * Is the readline library on the system? + */ + private readonly bool $readlineSupport; + + public function __construct() + { + // Readline is an extension for PHP that makes interactivity with PHP + // much more bash-like. + // http://www.php.net/manual/en/readline.installation.php + $this->readlineSupport = extension_loaded('readline'); + } + + /** + * Get input from the shell, using readline or the standard STDIN + * + * Named options must be in the following formats: + * php index.php user -v --v -name=John --name=John + * + * @param string|null $prefix You may specify a string with which to prompt the user. + */ + public function input(?string $prefix = null): string + { + // readline() can't be tested. + if ($this->readlineSupport && ENVIRONMENT !== 'testing') { + return readline($prefix); // @codeCoverageIgnore + } + + echo $prefix; + + $input = fgets(fopen('php://stdin', 'rb')); + + if ($input === false) { + $input = ''; + } + + return $input; + } + + /** + * While the library is intended for use on CLI commands, + * commands can be called from controllers and elsewhere + * so we need a way to allow them to still work. + * + * For now, just echo the content, but look into a better + * solution down the road. + * + * @param resource $handle + */ + public function fwrite($handle, string $string): void + { + if (! is_cli()) { + echo $string; + + return; + } + + fwrite($handle, $string); + } +} diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index 6402b002866d..fb60d73c5cd7 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -1,5 +1,7 @@ - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Cache\Exceptions; - -/** - * Provides a domain-level interface for broad capture - * of all framework-related exceptions. - * - * catch (\CodeIgniter\Cache\Exceptions\ExceptionInterface) { ... } - * - * @deprecated 4.1.2 - */ -interface ExceptionInterface -{ -} diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php index d78d0b1b02a4..e4b7488f43df 100644 --- a/system/Cache/FactoriesCache.php +++ b/system/Cache/FactoriesCache.php @@ -1,5 +1,7 @@ unserialize($data['__ci_value']), + // Yes, 'double' is returned and NOT 'float' + 'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null, + default => null, + }; } /** diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index 953de2dc20be..9e1003d10ac2 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -1,5 +1,7 @@ unserialize($data['__ci_value']), + // Yes, 'double' is returned and NOT 'float' + 'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null, + default => null, + }; } /** diff --git a/system/Cache/Handlers/WincacheHandler.php b/system/Cache/Handlers/WincacheHandler.php index b1ea45ded7a2..0ddee50a7fde 100644 --- a/system/Cache/Handlers/WincacheHandler.php +++ b/system/Cache/Handlers/WincacheHandler.php @@ -1,5 +1,7 @@ cacheQueryString = $config->cacheQueryString; - $this->cache = $cache; } /** @@ -83,7 +83,7 @@ public function generateCacheKey($request): string ? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : []) : ''; - return md5($uri->setFragment('')->setQuery($query)); + return md5($request->getMethod() . ':' . $uri->setFragment('')->setQuery($query)); } /** @@ -99,8 +99,14 @@ public function make($request, ResponseInterface $response): bool $headers = []; - foreach ($response->headers() as $header) { - $headers[$header->getName()] = $header->getValueLine(); + foreach ($response->headers() as $name => $value) { + if ($value instanceof Header) { + $headers[$name] = $value->getValueLine(); + } else { + foreach ($value as $header) { + $headers[$name][] = $header->getValueLine(); + } + } } return $this->cache->save( diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 6a2dca68c1e5..7fdade26fb99 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -17,10 +17,12 @@ use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Filters\Filters; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\ResponsableInterface; @@ -54,7 +56,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.4.8'; + public const CI_VERSION = '4.5.0'; /** * App startup time. @@ -135,25 +137,6 @@ class CodeIgniter */ protected static $cacheTTL = 0; - /** - * Request path to use. - * - * @var string|null - * - * @deprecated No longer used. - */ - protected $path; - - /** - * Should the Response instance "pretend" - * to keep from setting headers/cookies/etc - * - * @var bool - * - * @deprecated No longer used. - */ - protected $useSafeOutput = false; - /** * Context * web: Invoked by HTTP request @@ -171,7 +154,7 @@ class CodeIgniter /** * Whether to return Response object or send response. * - * @deprecated No longer used. + * @deprecated 4.4.0 No longer used. */ protected bool $returnResponse = false; @@ -203,24 +186,11 @@ public function __construct(App $config) */ public function initialize() { - // Define environment variables - $this->bootstrapEnvironment(); - - // Setup Exception Handling - Services::exceptions()->initialize(); - - // Run this check for manual installations - if (! is_file(COMPOSER_PATH)) { - $this->resolvePlatformExtensions(); // @codeCoverageIgnore - } - // Set default locale on the server Locale::setDefault($this->config->defaultLocale ?? 'en'); // Set default timezone on the server date_default_timezone_set($this->config->appTimezone ?? 'UTC'); - - $this->initializeKint(); } /** @@ -231,6 +201,8 @@ public function initialize() * @throws FrameworkException * * @codeCoverageIgnore + * + * @deprecated 4.5.0 Moved to system/bootstrap.php. */ protected function resolvePlatformExtensions() { @@ -257,6 +229,8 @@ protected function resolvePlatformExtensions() * Initializes Kint * * @return void + * + * @deprecated 4.5.0 Moved to Autoloader. */ protected function initializeKint() { @@ -272,6 +246,9 @@ protected function initializeKint() helper('kint'); } + /** + * @deprecated 4.5.0 Moved to Autoloader. + */ private function autoloadKint(): void { // If we have KINT_DIR it means it's already loaded via composer @@ -294,9 +271,12 @@ private function autoloadKint(): void } } + /** + * @deprecated 4.5.0 Moved to Autoloader. + */ private function configureKint(): void { - $config = config(KintConfig::class); + $config = new KintConfig(); Kint::$depth_limit = $config->maxDepth; Kint::$display_called_from = $config->displayCalledFrom; @@ -336,6 +316,8 @@ private function configureKint(): void * tries to route the response, loads the controller and generally * makes all the pieces work together. * + * @param bool $returnResponse Used for testing purposes only. + * * @return ResponseInterface|void */ public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false) @@ -355,25 +337,43 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon $this->getRequestObject(); $this->getResponseObject(); - $this->spoofRequestMethod(); + Events::trigger('pre_system'); - try { - $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); - } catch (ResponsableInterface|DeprecatedRedirectException $e) { - $this->outputBufferingEnd(); - if ($e instanceof DeprecatedRedirectException) { - $e = new RedirectException($e->getMessage(), $e->getCode(), $e); - } + $this->benchmark->stop('bootstrap'); + + $this->benchmark->start('required_before_filters'); + // Start up the filters + $filters = Services::filters(); + // Run required before filters + $possibleResponse = $this->runRequiredBeforeFilters($filters); - $this->response = $e->getResponse(); - } catch (PageNotFoundException $e) { - $this->response = $this->display404errors($e); - } catch (Throwable $e) { - $this->outputBufferingEnd(); + // If a ResponseInterface instance is returned then send it back to the client and stop + if ($possibleResponse instanceof ResponseInterface) { + $this->response = $possibleResponse; + } else { + try { + $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); + } catch (ResponsableInterface|DeprecatedRedirectException $e) { + $this->outputBufferingEnd(); + if ($e instanceof DeprecatedRedirectException) { + $e = new RedirectException($e->getMessage(), $e->getCode(), $e); + } + + $this->response = $e->getResponse(); + } catch (PageNotFoundException $e) { + $this->response = $this->display404errors($e); + } catch (Throwable $e) { + $this->outputBufferingEnd(); - throw $e; + throw $e; + } } + $this->runRequiredAfterFilters($filters); + + // Is there a post-system event? + Events::trigger('post_system'); + if ($returnResponse) { return $this->response; } @@ -382,19 +382,36 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon } /** - * Set our Response instance to "pretend" mode so that things like - * cookies and headers are not actually sent, allowing PHP 7.2+ to - * not complain when ini_set() function is used. - * - * @return $this - * - * @deprecated No longer used. + * Run required before filters. */ - public function useSafeOutput(bool $safe = true) + private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface { - $this->useSafeOutput = $safe; + $possibleResponse = $filters->runRequired('before'); + $this->benchmark->stop('required_before_filters'); - return $this; + // If a ResponseInterface instance is returned then send it back to the client and stop + if ($possibleResponse instanceof ResponseInterface) { + return $possibleResponse; + } + + return null; + } + + /** + * Run required after filters. + */ + private function runRequiredAfterFilters(Filters $filters): void + { + $filters->setResponse($this->response); + + // Run required after filters + $this->benchmark->start('required_after_filters'); + $response = $filters->runRequired('after'); + $this->benchmark->stop('required_after_filters'); + + if ($response instanceof ResponseInterface) { + $this->response = $response; + } } /** @@ -433,41 +450,30 @@ public function disableFilters(): void */ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false) { - $this->forceSecureAccess(); - - if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') { + if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') { return $this->response->setStatusCode(405)->setBody('Method Not Allowed'); } - Events::trigger('pre_system'); - - // Check for a cached page. Execution will stop - // if the page has been cached. - if (($response = $this->displayCache($cacheConfig)) instanceof ResponseInterface) { - return $response; - } - - $routeFilter = $this->tryToRouteIt($routes); + $routeFilters = $this->tryToRouteIt($routes); // $uri is URL-encoded. - $uri = $this->determinePath(); + $uri = $this->request->getPath(); if ($this->enableFilters) { - // Start up the filters - $filters = Services::filters(); + /** @var Filters $filters */ + $filters = service('filters'); // If any filters were specified within the routes file, // we need to ensure it's active for the current request - if ($routeFilter !== null) { - $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false; - if ($multipleFiltersEnabled) { - $filters->enableFilters($routeFilter, 'before'); - $filters->enableFilters($routeFilter, 'after'); - } else { - // for backward compatibility - $filters->enableFilter($routeFilter, 'before'); - $filters->enableFilter($routeFilter, 'after'); + if ($routeFilters !== null) { + $filters->enableFilters($routeFilters, 'before'); + + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if (! $oldFilterOrder) { + $routeFilters = array_reverse($routeFilters); } + + $filters->enableFilters($routeFilters, 'after'); } // Run "before" filters @@ -512,12 +518,10 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache $this->gatherOutput($cacheConfig, $returned); if ($this->enableFilters) { - $filters = Services::filters(); + /** @var Filters $filters */ + $filters = service('filters'); $filters->setResponse($this->response); - // After filter debug toolbar requires 'total_execution'. - $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); - // Run "after" filters $this->benchmark->start('after_filters'); $response = $filters->run($uri, 'after'); @@ -533,18 +537,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache ! $this->response instanceof DownloadResponse && ! $this->response instanceof RedirectResponse ) { - // Cache it without the performance metrics replaced - // so that we can have live speed updates along the way. - // Must be run after filters to preserve the Response headers. - $this->pageCache->make($this->request, $this->response); - - // Update the performance metrics - $body = $this->response->getBody(); - if ($body !== null) { - $output = $this->displayPerformanceMetrics($body); - $this->response->setBody($output); - } - // Save our current URI as the previous URI in the session // for safer, more accurate use with `previous_url()` helper function. $this->storePreviousURL(current_url(true)); @@ -552,9 +544,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache unset($uri); - // Is there a post-system event? - Events::trigger('post_system'); - return $this->response; } @@ -590,6 +579,8 @@ protected function detectEnvironment() * is wrong. At the very least, they should have error reporting setup. * * @return void + * + * @deprecated 4.5.0 Moved to system/bootstrap.php. */ protected function bootstrapEnvironment() { @@ -631,6 +622,9 @@ protected function startBenchmark() * @param CLIRequest|IncomingRequest $request * * @return $this + * + * @internal Used for testing purposes only. + * @testTag */ public function setRequest($request) { @@ -647,6 +641,8 @@ public function setRequest($request) protected function getRequestObject() { if ($this->request instanceof Request) { + $this->spoofRequestMethod(); + return; } @@ -656,7 +652,9 @@ protected function getRequestObject() Services::createRequest($this->config); } - $this->request = Services::request(); + $this->request = service('request'); + + $this->spoofRequestMethod(); } /** @@ -688,6 +686,8 @@ protected function getResponseObject() * should be enforced for this URL. * * @return void + * + * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter. */ protected function forceSecureAccess($duration = 31_536_000) { @@ -705,6 +705,7 @@ protected function forceSecureAccess($duration = 31_536_000) * * @throws Exception * + * @deprecated 4.5.0 PageCache required filter is used. No longer used. * @deprecated 4.4.2 The parameter $config is deprecated. No longer used. */ public function displayCache(Cache $config) @@ -759,6 +760,9 @@ public function cachePage(Cache $config) */ public function getPerformanceStats(): array { + // After filter debug toolbar requires 'total_execution'. + $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); + return [ 'startTime' => $this->startTime, 'totalTime' => $this->totalTime, @@ -782,15 +786,21 @@ protected function generateCacheName(Cache $config): string ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : []) : ''; - return md5($uri->setFragment('')->setQuery($query)); + return md5((string) $uri->setFragment('')->setQuery($query)); } /** - * Replaces the elapsed_time tag. + * Replaces the elapsed_time and memory_usage tag. + * + * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used. */ public function displayPerformanceMetrics(string $output): string { - return str_replace('{elapsed_time}', (string) $this->totalTime, $output); + return str_replace( + ['{elapsed_time}', '{memory_usage}'], + [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)], + $output + ); } /** @@ -807,22 +817,21 @@ public function displayPerformanceMetrics(string $output): string */ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) { + $this->benchmark->start('routing'); + if ($routes === null) { - $routes = Services::routes()->loadRoutes(); + $routes = service('routes')->loadRoutes(); } // $routes is defined in Config/Routes.php $this->router = Services::router($routes, $this->request); - // $path is URL-encoded. - $path = $this->determinePath(); - - $this->benchmark->stop('bootstrap'); - $this->benchmark->start('routing'); + // $uri is URL-encoded. + $uri = $this->request->getPath(); $this->outputBufferingStart(); - $this->controller = $this->router->handle($path); + $this->controller = $this->router->handle($uri); $this->method = $this->router->methodName(); // If a {locale} segment was matched in the final route, @@ -833,12 +842,6 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) $this->benchmark->stop('routing'); - // for backward compatibility - $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false; - if (! $multipleFiltersEnabled) { - return $this->router->getFilter(); - } - return $this->router->getFilters(); } @@ -847,30 +850,12 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) * on the CLI/IncomingRequest path. * * @return string - */ - protected function determinePath() - { - return $this->path ?? - (method_exists($this->request, 'getPath') - ? $this->request->getPath() - : $this->request->getUri()->getPath()); - } - - /** - * Allows the request path to be set from outside the class, - * instead of relying on CLIRequest or IncomingRequest for the path. - * - * This is not used now. * - * @return $this - * - * @deprecated No longer used. + * @deprecated 4.5.0 No longer used. */ - public function setPath(string $path) + protected function determinePath() { - $this->path = $path; - - return $this; + return $this->request->getPath(); } /** @@ -886,7 +871,7 @@ protected function startController() $this->benchmark->start('controller_constructor'); // Is it routed to a Closure? - if (is_object($this->controller) && (get_class($this->controller) === 'Closure')) { + if (is_object($this->controller) && ($this->controller::class === 'Closure')) { $controller = $this->controller; return $controller(...$this->router->params()); @@ -898,7 +883,10 @@ protected function startController() } // Try to autoload the class - if (! class_exists($this->controller, true) || $this->method[0] === '_') { + if ( + ! class_exists($this->controller, true) + || ($this->method[0] === '_' && $this->method !== '__invoke') + ) { throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } } @@ -938,6 +926,8 @@ protected function runController($class) // This is a Web request or PHP CLI request $params = $this->router->params(); + // The controller method param types may not be string. + // So cannot set `declare(strict_types=1)` in this file. $output = method_exists($class, '_remap') ? $class->_remap($this->method, ...$params) : $class->{$this->method}(...$params); @@ -955,6 +945,8 @@ protected function runController($class) */ protected function display404errors(PageNotFoundException $e) { + $this->response->setStatusCode($e->getCode()); + // Is there a 404 Override available? if ($override = $this->router->get404Override()) { $returned = null; @@ -969,7 +961,10 @@ protected function display404errors(PageNotFoundException $e) $this->method = $override[1]; $controller = $this->createController(); - $returned = $this->runController($controller); + + $returned = $controller->{$this->method}($e->getMessage()); + + $this->benchmark->stop('controller'); } unset($override); @@ -980,9 +975,6 @@ protected function display404errors(PageNotFoundException $e) return $this->response; } - // Display 404 Errors - $this->response->setStatusCode($e->getCode()); - $this->outputBufferingEnd(); // Throws new PageNotFoundException and remove exception message on production. @@ -1057,7 +1049,7 @@ public function storePreviousURL($uri) } // Ignore non-HTML responses - if (strpos($this->response->getHeaderLine('Content-Type'), 'text/html') === false) { + if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) { return; } @@ -1086,7 +1078,7 @@ public function storePreviousURL($uri) public function spoofRequestMethod() { // Only works with POSTED forms - if (strtolower($this->request->getMethod()) !== 'post') { + if ($this->request->getMethod() !== Method::POST) { return; } @@ -1097,7 +1089,7 @@ public function spoofRequestMethod() } // Only allows PUT, PATCH, DELETE - if (in_array(strtoupper($method), ['PUT', 'PATCH', 'DELETE'], true)) { + if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) { $this->request = $this->request->setMethod($method); } } diff --git a/system/Commands/Cache/ClearCache.php b/system/Commands/Cache/ClearCache.php index 79fab9dfae3c..e1180c28c6bd 100644 --- a/system/Commands/Cache/ClearCache.php +++ b/system/Commands/Cache/ClearCache.php @@ -1,5 +1,7 @@ {$group}['database'] = $name; if ($name !== ':memory:') { - $dbName = strpos($name, DIRECTORY_SEPARATOR) === false ? WRITEPATH . $name : $name; + $dbName = ! str_contains($name, DIRECTORY_SEPARATOR) ? WRITEPATH . $name : $name; if (is_file($dbName)) { CLI::error("Database \"{$dbName}\" already exists.", 'light_gray', 'red'); diff --git a/system/Commands/Database/Migrate.php b/system/Commands/Database/Migrate.php index 119335170d5e..b422e4a6db73 100644 --- a/system/Commands/Database/Migrate.php +++ b/system/Commands/Database/Migrate.php @@ -1,5 +1,7 @@ clearCliMessages(); CLI::write(lang('Migrations.latest'), 'yellow'); diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php index f683219e9d2c..e5e8a6d967f9 100644 --- a/system/Commands/Database/MigrateRefresh.php +++ b/system/Commands/Database/MigrateRefresh.php @@ -1,5 +1,7 @@ getLastBatch() - 1; diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index ecd074278d47..9506c2eae082 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -1,5 +1,7 @@ getNamespace(); + $namespaces = service('autoloader')->getNamespace(); // Collection of migration status $status = []; @@ -115,7 +116,7 @@ public function run(array $params) ksort($migrations); foreach ($migrations as $uid => $migration) { - $migrations[$uid]->name = mb_substr($migration->name, mb_strpos($migration->name, $uid . '_')); + $migrations[$uid]->name = mb_substr($migration->name, (int) mb_strpos($migration->name, $uid . '_')); $date = '---'; $group = '---'; @@ -127,7 +128,7 @@ public function run(array $params) continue; } - $date = date('Y-m-d H:i:s', $row->time); + $date = date('Y-m-d H:i:s', (int) $row->time); $group = $row->group; $batch = $row->batch; // @codeCoverageIgnoreEnd diff --git a/system/Commands/Database/Seed.php b/system/Commands/Database/Seed.php index adfdd0a5f701..fc1c0f1ae62e 100644 --- a/system/Commands/Database/Seed.php +++ b/system/Commands/Database/Seed.php @@ -1,5 +1,7 @@ 'Sorts the table rows in DESC order.', '--limit-rows' => 'Limits the number of rows. Default: 10.', '--limit-field-value' => 'Limits the length of field values. Default: 15.', + '--dbgroup' => 'Database group to show.', ]; /** @@ -88,7 +92,7 @@ class ShowTableInfo extends BaseCommand */ private array $tbody; - private BaseConnection $db; + private ?BaseConnection $db = null; /** * @var bool Sort the table rows in DESC order or not. @@ -99,9 +103,20 @@ class ShowTableInfo extends BaseCommand public function run(array $params) { - $this->db = Database::connect(); + $dbGroup = $params['dbgroup'] ?? CLI::getOption('dbgroup'); + + try { + $this->db = Database::connect($dbGroup); + } catch (InvalidArgumentException $e) { + CLI::error($e->getMessage()); + + return EXIT_ERROR; + } + $this->DBPrefix = $this->db->getPrefix(); + $this->showDBConfig(); + $tables = $this->db->listTables(); if (array_key_exists('desc', $params)) { @@ -112,13 +127,13 @@ public function run(array $params) CLI::error('Database has no tables!', 'light_gray', 'red'); CLI::newLine(); - return; + return EXIT_ERROR; } if (array_key_exists('show', $params)) { $this->showAllTables($tables); - return; + return EXIT_ERROR; } $tableName = $params[0] ?? null; @@ -139,10 +154,28 @@ public function run(array $params) if (array_key_exists('metadata', $params)) { $this->showFieldMetaData($tableName); - return; + return EXIT_SUCCESS; } $this->showDataOfTable($tableName, $limitRows, $limitFieldValue); + + return EXIT_SUCCESS; + } + + private function showDBConfig(): void + { + $data = [[ + 'hostname' => $this->db->hostname, + 'database' => $this->db->getDatabase(), + 'username' => $this->db->username, + 'DBDriver' => $this->db->getPlatform(), + 'DBPrefix' => $this->DBPrefix, + 'port' => $this->db->port, + ]]; + CLI::table( + $data, + ['hostname', 'database', 'username', 'DBDriver', 'DBPrefix', 'port'] + ); } private function removeDBPrefix(): void @@ -274,9 +307,12 @@ private function showFieldMetaData(string $tableName): void CLI::table($this->tbody, $thead); } - private function setYesOrNo(bool $fieldValue): string + /** + * @param bool|int|string|null $fieldValue + */ + private function setYesOrNo($fieldValue): string { - if ($fieldValue) { + if ((bool) $fieldValue) { return CLI::color('Yes', 'green'); } diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index 419ae6ef3e34..820ec48137c9 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -1,5 +1,7 @@ null]); + $this->templatePath = config(Generators::class)->views[$this->name]['class']; $this->template = 'cell.tpl.php'; $this->classNameLang = 'CLI.generator.className.cell'; + $this->generateClass($params); - $this->name = 'make:cell_view'; + $this->templatePath = config(Generators::class)->views[$this->name]['view']; $this->template = 'cell_view.tpl.php'; $this->classNameLang = 'CLI.generator.viewName.cell'; $className = $this->qualifyClassName(); $viewName = decamelize(class_basename($className)); - $viewName = preg_replace('/([a-z][a-z0-9_\/\\\\]+)(_cell)$/i', '$1', $viewName) ?? $viewName; + $viewName = preg_replace( + '/([a-z][a-z0-9_\/\\\\]+)(_cell)$/i', + '$1', + $viewName + ) ?? $viewName; $namespace = substr($className, 0, strrpos($className, '\\') + 1); $this->generateView($namespace . $viewName, $params); diff --git a/system/Commands/Generators/CommandGenerator.php b/system/Commands/Generators/CommandGenerator.php index b844666a794e..8c2ebcfb5d44 100644 --- a/system/Commands/Generators/CommandGenerator.php +++ b/system/Commands/Generators/CommandGenerator.php @@ -1,5 +1,7 @@ template = 'command.tpl.php'; $this->classNameLang = 'CLI.generator.className.command'; - $this->execute($params); + $this->generateClass($params); } /** diff --git a/system/Commands/Generators/ConfigGenerator.php b/system/Commands/Generators/ConfigGenerator.php index a83a9671201d..7b1d5f21ff45 100644 --- a/system/Commands/Generators/ConfigGenerator.php +++ b/system/Commands/Generators/ConfigGenerator.php @@ -1,5 +1,7 @@ template = 'config.tpl.php'; $this->classNameLang = 'CLI.generator.className.config'; - $this->execute($params); + $this->generateClass($params); } /** diff --git a/system/Commands/Generators/ControllerGenerator.php b/system/Commands/Generators/ControllerGenerator.php index 2cf912b1c7ff..c6f54cac2888 100644 --- a/system/Commands/Generators/ControllerGenerator.php +++ b/system/Commands/Generators/ControllerGenerator.php @@ -1,5 +1,7 @@ template = 'controller.tpl.php'; $this->classNameLang = 'CLI.generator.className.controller'; - $this->execute($params); + $this->generateClass($params); } /** diff --git a/system/Commands/Generators/EntityGenerator.php b/system/Commands/Generators/EntityGenerator.php index bd20daf59662..09c350560638 100644 --- a/system/Commands/Generators/EntityGenerator.php +++ b/system/Commands/Generators/EntityGenerator.php @@ -1,5 +1,7 @@ template = 'entity.tpl.php'; $this->classNameLang = 'CLI.generator.className.entity'; - $this->execute($params); + $this->generateClass($params); } } diff --git a/system/Commands/Generators/FilterGenerator.php b/system/Commands/Generators/FilterGenerator.php index 620bee5a9ad4..c723da3afab7 100644 --- a/system/Commands/Generators/FilterGenerator.php +++ b/system/Commands/Generators/FilterGenerator.php @@ -1,5 +1,7 @@ template = 'filter.tpl.php'; $this->classNameLang = 'CLI.generator.className.filter'; - $this->execute($params); + $this->generateClass($params); } } diff --git a/system/Commands/Generators/MigrateCreate.php b/system/Commands/Generators/MigrateCreate.php deleted file mode 100644 index a2fa6bf76667..000000000000 --- a/system/Commands/Generators/MigrateCreate.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Commands\Generators; - -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\CLI; - -/** - * Deprecated class for the migration creation command. - * - * @deprecated Use make:migration instead. - * - * @codeCoverageIgnore - */ -class MigrateCreate extends BaseCommand -{ - /** - * The group the command is lumped under - * when listing commands. - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's name - * - * @var string - */ - protected $name = 'migrate:create'; - - /** - * The Command's short description - * - * @var string - */ - protected $description = '[DEPRECATED] Creates a new migration file. Please use "make:migration" instead.'; - - /** - * The Command's usage - * - * @var string - */ - protected $usage = 'migrate:create [options]'; - - /** - * The Command's arguments. - * - * @var array - */ - protected $arguments = [ - 'name' => 'The migration file name.', - ]; - - /** - * The Command's options. - * - * @var array - */ - protected $options = [ - '--namespace' => 'Set root namespace. Defaults to APP_NAMESPACE', - '--force' => 'Force overwrite existing files.', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - // Resolve arguments before passing to make:migration - $params[0] ??= CLI::getSegment(2); - - $params['namespace'] ??= CLI::getOption('namespace') ?? APP_NAMESPACE; - - if (array_key_exists('force', $params) || CLI::getOption('force')) { - $params['force'] = null; - } - - $this->call('make:migration', $params); - } -} diff --git a/system/Commands/Generators/MigrationGenerator.php b/system/Commands/Generators/MigrationGenerator.php index 52f9e6e53535..b7d7d585e945 100644 --- a/system/Commands/Generators/MigrationGenerator.php +++ b/system/Commands/Generators/MigrationGenerator.php @@ -1,5 +1,7 @@ classNameLang = 'CLI.generator.className.migration'; - $this->execute($params); + $this->generateClass($params); } /** diff --git a/system/Commands/Generators/ModelGenerator.php b/system/Commands/Generators/ModelGenerator.php index f4946a9441c9..5450bda79b4c 100644 --- a/system/Commands/Generators/ModelGenerator.php +++ b/system/Commands/Generators/ModelGenerator.php @@ -1,5 +1,7 @@ template = 'model.tpl.php'; $this->classNameLang = 'CLI.generator.className.model'; - $this->execute($params); + $this->generateClass($params); } /** diff --git a/system/Commands/Generators/ScaffoldGenerator.php b/system/Commands/Generators/ScaffoldGenerator.php index ef34b92ed572..3b1ef7957c9b 100644 --- a/system/Commands/Generators/ScaffoldGenerator.php +++ b/system/Commands/Generators/ScaffoldGenerator.php @@ -1,5 +1,7 @@ template = 'seeder.tpl.php'; $this->classNameLang = 'CLI.generator.className.seeder'; - $this->execute($params); + $this->generateClass($params); } } diff --git a/system/Commands/Generators/SessionMigrationGenerator.php b/system/Commands/Generators/SessionMigrationGenerator.php deleted file mode 100644 index cb7da5892780..000000000000 --- a/system/Commands/Generators/SessionMigrationGenerator.php +++ /dev/null @@ -1,113 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Commands\Generators; - -use CodeIgniter\CLI\BaseCommand; -use CodeIgniter\CLI\CLI; -use CodeIgniter\CLI\GeneratorTrait; -use Config\App; -use Config\Migrations; - -/** - * Generates a migration file for database sessions. - * - * @deprecated Use `make:migration --session` instead. - * - * @codeCoverageIgnore - */ -class SessionMigrationGenerator extends BaseCommand -{ - use GeneratorTrait; - - /** - * The Command's Group - * - * @var string - */ - protected $group = 'Generators'; - - /** - * The Command's Name - * - * @var string - */ - protected $name = 'session:migration'; - - /** - * The Command's Description - * - * @var string - */ - protected $description = '[DEPRECATED] Generates the migration file for database sessions, Please use "make:migration --session" instead.'; - - /** - * The Command's Usage - * - * @var string - */ - protected $usage = 'session:migration [options]'; - - /** - * The Command's Options - * - * @var array - */ - protected $options = [ - '-t' => 'Supply a table name.', - '-g' => 'Database group to use. Default: "default".', - ]; - - /** - * Actually execute a command. - */ - public function run(array $params) - { - $this->component = 'Migration'; - $this->directory = 'Database\Migrations'; - $this->template = 'migration.tpl.php'; - - $table = 'ci_sessions'; - - if (array_key_exists('t', $params) || CLI::getOption('t')) { - $table = $params['t'] ?? CLI::getOption('t'); - } - - $params[0] = "_create_{$table}_table"; - - $this->execute($params); - } - - /** - * Performs the necessary replacements. - */ - protected function prepare(string $class): string - { - $data = []; - $data['session'] = true; - $data['table'] = $this->getOption('t'); - $data['DBGroup'] = $this->getOption('g'); - $data['matchIP'] = config(App::class)->sessionMatchIP ?? false; - - $data['table'] = is_string($data['table']) ? $data['table'] : 'ci_sessions'; - $data['DBGroup'] = is_string($data['DBGroup']) ? $data['DBGroup'] : 'default'; - - return $this->parseTemplate($class, [], [], $data); - } - - /** - * Change file basename before saving. - */ - protected function basename(string $filename): string - { - return gmdate(config(Migrations::class)->timestampFormat) . basename($filename); - } -} diff --git a/system/Commands/Generators/TestGenerator.php b/system/Commands/Generators/TestGenerator.php new file mode 100644 index 000000000000..35019c72b826 --- /dev/null +++ b/system/Commands/Generators/TestGenerator.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Generators; + +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\CLI\GeneratorTrait; + +/** + * Generates a skeleton command file. + */ +class TestGenerator extends BaseCommand +{ + use GeneratorTrait; + + /** + * The Command's Group + * + * @var string + */ + protected $group = 'Generators'; + + /** + * The Command's Name + * + * @var string + */ + protected $name = 'make:test'; + + /** + * The Command's Description + * + * @var string + */ + protected $description = 'Generates a new test file.'; + + /** + * The Command's Usage + * + * @var string + */ + protected $usage = 'make:test [options]'; + + /** + * The Command's Arguments + * + * @var array + */ + protected $arguments = [ + 'name' => 'The test class name.', + ]; + + /** + * The Command's Options + * + * @var array + */ + protected $options = [ + '--namespace' => 'Set root namespace. Default: "Tests".', + '--force' => 'Force overwrite existing file.', + ]; + + /** + * Actually execute a command. + */ + public function run(array $params) + { + $this->component = 'Test'; + $this->template = 'test.tpl.php'; + + $this->classNameLang = 'CLI.generator.className.test'; + + $autoload = service('autoloader'); + $autoload->addNamespace('CodeIgniter', TESTPATH . 'system'); + $autoload->addNamespace('Tests', ROOTPATH . 'tests'); + + $this->generateClass($params); + } + + /** + * Gets the namespace from input or the default namespace. + */ + protected function getNamespace(): string + { + if ($this->namespace !== null) { + return $this->namespace; + } + + if ($this->getOption('namespace') !== null) { + return trim( + str_replace( + '/', + '\\', + $this->getOption('namespace') + ), + '\\' + ); + } + + $class = $this->normalizeInputClassName(); + $classPaths = explode('\\', $class); + + $namespaces = service('autoloader')->getNamespace(); + + while ($classPaths !== []) { + array_pop($classPaths); + $namespace = implode('\\', $classPaths); + + foreach (array_keys($namespaces) as $prefix) { + if ($prefix === $namespace) { + // The input classname is FQCN, and use the namespace. + return $namespace; + } + } + } + + return 'Tests'; + } + + /** + * Builds the test file path from the class name. + * + * @param string $class namespaced classname. + */ + protected function buildPath(string $class): string + { + $namespace = $this->getNamespace(); + + $base = $this->searchTestFilePath($namespace); + + if ($base === null) { + CLI::error( + lang('CLI.namespaceNotDefined', [$namespace]), + 'light_gray', + 'red' + ); + CLI::newLine(); + + return ''; + } + + $realpath = realpath($base); + $base = ($realpath !== false) ? $realpath : $base; + + $file = $base . DIRECTORY_SEPARATOR + . str_replace( + '\\', + DIRECTORY_SEPARATOR, + trim(str_replace($namespace . '\\', '', $class), '\\') + ) . '.php'; + + return implode( + DIRECTORY_SEPARATOR, + array_slice( + explode(DIRECTORY_SEPARATOR, $file), + 0, + -1 + ) + ) . DIRECTORY_SEPARATOR . $this->basename($file); + } + + /** + * Returns test file path for the namespace. + */ + private function searchTestFilePath(string $namespace): ?string + { + $bases = service('autoloader')->getNamespace($namespace); + + $base = null; + + foreach ($bases as $candidate) { + if (str_contains($candidate, '/tests/')) { + $base = $candidate; + + break; + } + } + + return $base; + } +} diff --git a/system/Commands/Generators/ValidationGenerator.php b/system/Commands/Generators/ValidationGenerator.php index 1b2efb8db18a..d9e3d495a769 100644 --- a/system/Commands/Generators/ValidationGenerator.php +++ b/system/Commands/Generators/ValidationGenerator.php @@ -1,5 +1,7 @@ template = 'validation.tpl.php'; $this->classNameLang = 'CLI.generator.className.validation'; - $this->execute($params); + $this->generateClass($params); } } diff --git a/system/Commands/Generators/Views/model.tpl.php b/system/Commands/Generators/Views/model.tpl.php index 72509cdbd9d4..954404f854d4 100644 --- a/system/Commands/Generators/Views/model.tpl.php +++ b/system/Commands/Generators/Views/model.tpl.php @@ -18,6 +18,10 @@ class {class} extends Model protected $allowedFields = []; protected bool $allowEmptyInserts = false; + protected bool $updateOnlyChanged = true; + + protected array $casts = []; + protected array $castHandlers = []; // Dates protected $useTimestamps = false; diff --git a/system/Commands/Generators/Views/test.tpl.php b/system/Commands/Generators/Views/test.tpl.php new file mode 100644 index 000000000000..f67348d079f8 --- /dev/null +++ b/system/Commands/Generators/Views/test.tpl.php @@ -0,0 +1,18 @@ +<@php + +namespace {namespace}; + +use CodeIgniter\Test\CIUnitTestCase; + +class {class} extends CIUnitTestCase +{ + protected function setUp(): void + { + parent::setUp(); + } + + public function testExample(): void + { + // + } +} diff --git a/system/Commands/Help.php b/system/Commands/Help.php index 338a5c868c72..76913e8a33fc 100644 --- a/system/Commands/Help.php +++ b/system/Commands/Help.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Translation; + +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Helpers\Array\ArrayHelper; +use Config\App; +use Locale; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; + +/** + * @see \CodeIgniter\Commands\Translation\LocalizationFinderTest + */ +class LocalizationFinder extends BaseCommand +{ + protected $group = 'Translation'; + protected $name = 'lang:find'; + protected $description = 'Find and save available phrases to translate.'; + protected $usage = 'lang:find [options]'; + protected $arguments = []; + protected $options = [ + '--locale' => 'Specify locale (en, ru, etc.) to save files.', + '--dir' => 'Directory to search for translations relative to APPPATH.', + '--show-new' => 'Show only new translations in table. Does not write to files.', + '--verbose' => 'Output detailed information.', + ]; + + /** + * Flag for output detailed information + */ + private bool $verbose = false; + + /** + * Flag for showing only translations, without saving + */ + private bool $showNew = false; + + private string $languagePath; + + public function run(array $params) + { + $this->verbose = array_key_exists('verbose', $params); + $this->showNew = array_key_exists('show-new', $params); + $optionLocale = $params['locale'] ?? null; + $optionDir = $params['dir'] ?? null; + $currentLocale = Locale::getDefault(); + $currentDir = APPPATH; + $this->languagePath = $currentDir . 'Language'; + + if (ENVIRONMENT === 'testing') { + $currentDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR; + $this->languagePath = SUPPORTPATH . 'Language'; + } + + if (is_string($optionLocale)) { + if (! in_array($optionLocale, config(App::class)->supportedLocales, true)) { + CLI::error( + 'Error: "' . $optionLocale . '" is not supported. Supported locales: ' + . implode(', ', config(App::class)->supportedLocales) + ); + + return EXIT_USER_INPUT; + } + + $currentLocale = $optionLocale; + } + + if (is_string($optionDir)) { + $tempCurrentDir = realpath($currentDir . $optionDir); + + if ($tempCurrentDir === false) { + CLI::error('Error: Directory must be located in "' . $currentDir . '"'); + + return EXIT_USER_INPUT; + } + + if ($this->isSubDirectory($tempCurrentDir, $this->languagePath)) { + CLI::error('Error: Directory "' . $this->languagePath . '" restricted to scan.'); + + return EXIT_USER_INPUT; + } + + $currentDir = $tempCurrentDir; + } + + $this->process($currentDir, $currentLocale); + + CLI::write('All operations done!'); + + return EXIT_SUCCESS; + } + + private function process(string $currentDir, string $currentLocale): void + { + $tableRows = []; + $countNewKeys = 0; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($currentDir)); + $files = iterator_to_array($iterator, true); + ksort($files); + + [ + 'foundLanguageKeys' => $foundLanguageKeys, + 'badLanguageKeys' => $badLanguageKeys, + 'countFiles' => $countFiles + ] = $this->findLanguageKeysInFiles($files); + + ksort($foundLanguageKeys); + + $languageDiff = []; + $languageFoundGroups = array_unique(array_keys($foundLanguageKeys)); + + foreach ($languageFoundGroups as $langFileName) { + $languageStoredKeys = []; + $languageFilePath = $this->languagePath . DIRECTORY_SEPARATOR . $currentLocale . DIRECTORY_SEPARATOR . $langFileName . '.php'; + + if (is_file($languageFilePath)) { + // Load old localization + $languageStoredKeys = require $languageFilePath; + } + + $languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $languageStoredKeys); + $countNewKeys += ArrayHelper::recursiveCount($languageDiff); + + if ($this->showNew) { + $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows); + } else { + $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys); + + if ($languageDiff !== []) { + if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) { + $this->writeIsVerbose('Lang file ' . $langFileName . ' (error write).', 'red'); + } else { + $this->writeIsVerbose('Lang file "' . $langFileName . '" successful updated!', 'green'); + } + } + } + } + + if ($this->showNew && $tableRows !== []) { + sort($tableRows); + CLI::table($tableRows, ['File', 'Key']); + } + + if (! $this->showNew && $countNewKeys > 0) { + CLI::write('Note: You need to run your linting tool to fix coding standards issues.', 'white', 'red'); + } + + $this->writeIsVerbose('Files found: ' . $countFiles); + $this->writeIsVerbose('New translates found: ' . $countNewKeys); + $this->writeIsVerbose('Bad translates found: ' . count($badLanguageKeys)); + + if ($this->verbose && $badLanguageKeys !== []) { + $tableBadRows = []; + + foreach ($badLanguageKeys as $value) { + $tableBadRows[] = [$value[1], $value[0]]; + } + + ArrayHelper::sortValuesByNatural($tableBadRows, 0); + + CLI::table($tableBadRows, ['Bad Key', 'Filepath']); + } + } + + /** + * @param SplFileInfo|string $file + * + * @return array + */ + private function findTranslationsInFile($file): array + { + $foundLanguageKeys = []; + $badLanguageKeys = []; + + if (is_string($file) && is_file($file)) { + $file = new SplFileInfo($file); + } + + $fileContent = file_get_contents($file->getRealPath()); + preg_match_all('/lang\(\'([._a-z0-9\-]+)\'\)/ui', $fileContent, $matches); + + if ($matches[1] === []) { + return compact('foundLanguageKeys', 'badLanguageKeys'); + } + + foreach ($matches[1] as $phraseKey) { + $phraseKeys = explode('.', $phraseKey); + + // Language key not have Filename or Lang key + if (count($phraseKeys) < 2) { + $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey]; + + continue; + } + + $languageFileName = array_shift($phraseKeys); + $isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '') + || ($languageFileName === '' && $phraseKeys[0] !== '') + || ($languageFileName === '' && $phraseKeys[0] === ''); + + if ($isEmptyNestedArray) { + $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey]; + + continue; + } + + if (count($phraseKeys) === 1) { + $foundLanguageKeys[$languageFileName][$phraseKeys[0]] = $phraseKey; + } else { + $childKeys = $this->buildMultiArray($phraseKeys, $phraseKey); + + $foundLanguageKeys[$languageFileName] = array_replace_recursive($foundLanguageKeys[$languageFileName] ?? [], $childKeys); + } + } + + return compact('foundLanguageKeys', 'badLanguageKeys'); + } + + private function isIgnoredFile(SplFileInfo $file): bool + { + if ($file->isDir() || $this->isSubDirectory($file->getRealPath(), $this->languagePath)) { + return true; + } + + return $file->getExtension() !== 'php'; + } + + private function templateFile(array $language = []): string + { + if ($language !== []) { + $languageArrayString = var_export($language, true); + + $code = <<replaceArraySyntax($code); + } + + return <<<'PHP' + $token) { + if (is_array($token)) { + [$tokenId, $tokenValue] = $token; + + // Replace "array (" + if ( + $tokenId === T_ARRAY + && $tokens[$i + 1][0] === T_WHITESPACE + && $tokens[$i + 2] === '(' + ) { + $newTokens[$i][1] = '['; + $newTokens[$i + 1][1] = ''; + $newTokens[$i + 2] = ''; + } + + // Replace indent + if ($tokenId === T_WHITESPACE && preg_match('/\n([ ]+)/u', $tokenValue, $matches)) { + $newTokens[$i][1] = "\n{$matches[1]}{$matches[1]}"; + } + } // Replace ")" + elseif ($token === ')') { + $newTokens[$i] = ']'; + } + } + + $output = ''; + + foreach ($newTokens as $token) { + $output .= $token[1] ?? $token; + } + + return $output; + } + + /** + * Create multidimensional array from another keys + */ + private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array + { + $newArray = []; + $lastIndex = array_pop($fromKeys); + $current = &$newArray; + + foreach ($fromKeys as $value) { + $current[$value] = []; + $current = &$current[$value]; + } + + $current[$lastIndex] = $lastArrayValue; + + return $newArray; + } + + /** + * Convert multi arrays to specific CLI table rows (flat array) + */ + private function arrayToTableRows(string $langFileName, array $array): array + { + $rows = []; + + foreach ($array as $value) { + if (is_array($value)) { + $rows = array_merge($rows, $this->arrayToTableRows($langFileName, $value)); + + continue; + } + + if (is_string($value)) { + $rows[] = [$langFileName, $value]; + } + } + + return $rows; + } + + /** + * Show details in the console if the flag is set + */ + private function writeIsVerbose(string $text = '', ?string $foreground = null, ?string $background = null): void + { + if ($this->verbose) { + CLI::write($text, $foreground, $background); + } + } + + private function isSubDirectory(string $directory, string $rootDirectory): bool + { + return 0 === strncmp($directory, $rootDirectory, strlen($directory)); + } + + /** + * @param list $files + * + * @return array + * @phpstan-return array{'foundLanguageKeys': array>, 'badLanguageKeys': array>, 'countFiles': int} + */ + private function findLanguageKeysInFiles(array $files): array + { + $foundLanguageKeys = []; + $badLanguageKeys = []; + $countFiles = 0; + + foreach ($files as $file) { + if ($this->isIgnoredFile($file)) { + continue; + } + + $this->writeIsVerbose('File found: ' . mb_substr($file->getRealPath(), mb_strlen(APPPATH))); + $countFiles++; + + $findInFile = $this->findTranslationsInFile($file); + + $foundLanguageKeys = array_replace_recursive($findInFile['foundLanguageKeys'], $foundLanguageKeys); + $badLanguageKeys = array_merge($findInFile['badLanguageKeys'], $badLanguageKeys); + } + + return compact('foundLanguageKeys', 'badLanguageKeys', 'countFiles'); + } +} diff --git a/system/Commands/Utilities/ConfigCheck.php b/system/Commands/Utilities/ConfigCheck.php new file mode 100644 index 000000000000..7d6dc332ca45 --- /dev/null +++ b/system/Commands/Utilities/ConfigCheck.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities; + +use CodeIgniter\Cache\FactoriesCache; +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Config\BaseConfig; +use Config\Optimize; +use Kint\Kint; + +/** + * Check the Config values. + * + * @see \CodeIgniter\Commands\Utilities\ConfigCheckTest + */ +final class ConfigCheck extends BaseCommand +{ + /** + * The group the command is lumped under + * when listing commands. + * + * @var string + */ + protected $group = 'CodeIgniter'; + + /** + * The Command's name + * + * @var string + */ + protected $name = 'config:check'; + + /** + * The Command's short description + * + * @var string + */ + protected $description = 'Check your Config values.'; + + /** + * The Command's usage + * + * @var string + */ + protected $usage = 'config:check '; + + /** + * The Command's arguments + * + * @var array + */ + protected $arguments = [ + 'classname' => 'The config classname to check. Short classname or FQCN.', + ]; + + /** + * The Command's options + * + * @var array + */ + protected $options = []; + + /** + * {@inheritDoc} + */ + public function run(array $params) + { + if (! isset($params[0])) { + CLI::error('You must specify a Config classname.'); + CLI::write(' Usage: ' . $this->usage); + CLI::write('Example: config:check App'); + CLI::write(' config:check \'CodeIgniter\Shield\Config\Auth\''); + + return EXIT_ERROR; + } + + /** @var class-string $class */ + $class = $params[0]; + + // Load Config cache if it is enabled. + $configCacheEnabled = class_exists(Optimize::class) + && (new Optimize())->configCacheEnabled; + if ($configCacheEnabled) { + $factoriesCache = new FactoriesCache(); + $factoriesCache->load('config'); + } + + $config = config($class); + + if ($config === null) { + CLI::error('No such Config class: ' . $class); + + return EXIT_ERROR; + } + + if (defined('KINT_DIR') && Kint::$enabled_mode !== false) { + CLI::write($this->getKintD($config)); + } else { + CLI::write( + CLI::color($this->getVarDump($config), 'cyan') + ); + } + + CLI::newLine(); + $state = CLI::color($configCacheEnabled ? 'Enabled' : 'Disabled', 'green'); + CLI::write('Config Caching: ' . $state); + + return EXIT_SUCCESS; + } + + /** + * Gets object dump by Kint d() + */ + private function getKintD(object $config): string + { + ob_start(); + d($config); + $output = ob_get_clean(); + + $output = trim($output); + + $lines = explode("\n", $output); + array_splice($lines, 0, 3); + array_splice($lines, -3); + + return implode("\n", $lines); + } + + /** + * Gets object dump by var_dump() + */ + private function getVarDump(object $config): string + { + ob_start(); + var_dump($config); + $output = ob_get_clean(); + + return preg_replace( + '!.*system/Commands/Utilities/ConfigCheck.php.*\n!u', + '', + $output + ); + } +} diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php index fd8f68fabdbd..17a08d21d2ce 100644 --- a/system/Commands/Utilities/Environment.php +++ b/system/Commands/Utilities/Environment.php @@ -1,5 +1,7 @@ */ protected $arguments = [ - 'method' => 'The HTTP method. get, post, put, etc.', + 'method' => 'The HTTP method. GET, POST, PUT, etc.', 'route' => 'The route (URI path) to check filters.', ]; @@ -76,17 +77,17 @@ public function run(array $params) if (! isset($params[0], $params[1])) { CLI::error('You must specify a HTTP verb and a route.'); CLI::write(' Usage: ' . $this->usage); - CLI::write('Example: filter:check get /'); - CLI::write(' filter:check put products/1'); + CLI::write('Example: filter:check GET /'); + CLI::write(' filter:check PUT products/1'); return EXIT_ERROR; } - $method = strtolower($params[0]); + $method = $params[0]; $route = $params[1]; // Load Routes - Services::routes()->loadRoutes(); + service('routes')->loadRoutes(); $filterCollector = new FilterCollector(); @@ -106,6 +107,8 @@ public function run(array $params) return EXIT_ERROR; } + $filters = $this->addRequiredFilters($filterCollector, $filters); + $tbody[] = [ strtoupper($method), $route, @@ -124,4 +127,29 @@ public function run(array $params) return EXIT_SUCCESS; } + + private function addRequiredFilters(FilterCollector $filterCollector, array $filters): array + { + $output = []; + + $required = $filterCollector->getRequiredFilters(); + + $colored = []; + + foreach ($required['before'] as $filter) { + $filter = CLI::color($filter, 'yellow'); + $colored[] = $filter; + } + $output['before'] = array_merge($colored, $filters['before']); + + $colored = []; + + foreach ($required['after'] as $filter) { + $filter = CLI::color($filter, 'yellow'); + $colored[] = $filter; + } + $output['after'] = array_merge($filters['after'], $colored); + + return $output; + } } diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index 71461933c5a0..c16f692cfb37 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -1,5 +1,7 @@ psr4 as $ns => $paths) { - if (array_key_exists('r', $params)) { - $pathOutput = $this->truncate($paths, $maxLength); - } else { - $pathOutput = $this->truncate(clean_path($paths), $maxLength); - } - foreach ((array) $paths as $path) { + if (array_key_exists('r', $params)) { + $pathOutput = $this->truncate($path, $maxLength); + } else { + $pathOutput = $this->truncate(clean_path($path), $maxLength); + } + $path = realpath($path) ?: $path; $tbody[] = [ diff --git a/system/Commands/Utilities/Optimize.php b/system/Commands/Utilities/Optimize.php new file mode 100644 index 000000000000..fa7612d524a6 --- /dev/null +++ b/system/Commands/Utilities/Optimize.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities; + +use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorCached; +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Publisher\Publisher; +use RuntimeException; + +/** + * Optimize for production. + */ +final class Optimize extends BaseCommand +{ + /** + * The group the command is lumped under + * when listing commands. + * + * @var string + */ + protected $group = 'CodeIgniter'; + + /** + * The Command's name + * + * @var string + */ + protected $name = 'optimize'; + + /** + * The Command's short description + * + * @var string + */ + protected $description = 'Optimize for production.'; + + /** + * The Command's usage + * + * @var string + */ + protected $usage = 'optimize'; + + /** + * {@inheritDoc} + */ + public function run(array $params) + { + try { + $this->enableCaching(); + $this->clearCache(); + $this->removeDevPackages(); + } catch (RuntimeException) { + CLI::error('The "spark optimize" failed.'); + + return EXIT_ERROR; + } + + return EXIT_SUCCESS; + } + + private function clearCache(): void + { + $locator = new FileLocatorCached(new FileLocator(service('autoloader'))); + $locator->deleteCache(); + CLI::write('Removed FileLocatorCache.', 'green'); + + $cache = WRITEPATH . 'cache/FactoriesCache_config'; + $this->removeFile($cache); + } + + private function removeFile(string $cache): void + { + if (is_file($cache)) { + $result = unlink($cache); + + if ($result) { + CLI::write('Removed "' . clean_path($cache) . '".', 'green'); + + return; + } + + CLI::error('Error in removing file: ' . clean_path($cache)); + + throw new RuntimeException(__METHOD__); + } + } + + private function enableCaching(): void + { + $publisher = new Publisher(APPPATH, APPPATH); + + $config = APPPATH . 'Config/Optimize.php'; + + $result = $publisher->replace( + $config, + [ + 'public bool $configCacheEnabled = false;' => 'public bool $configCacheEnabled = true;', + 'public bool $locatorCacheEnabled = false;' => 'public bool $locatorCacheEnabled = true;', + ] + ); + + if ($result) { + CLI::write( + 'Config Caching and FileLocator Caching are enabled in "app/Config/Optimize.php".', + 'green' + ); + + return; + } + + CLI::error('Error in updating file: ' . clean_path($config)); + + throw new RuntimeException(__METHOD__); + } + + private function removeDevPackages(): void + { + if (! defined('VENDORPATH')) { + return; + } + + chdir(ROOTPATH); + passthru('composer install --no-dev', $status); + + if ($status === 0) { + CLI::write('Removed Composer dev packages.', 'green'); + + return; + } + + CLI::error('Error in removing Composer dev packages.'); + + throw new RuntimeException(__METHOD__); + } +} diff --git a/system/Commands/Utilities/PhpIniCheck.php b/system/Commands/Utilities/PhpIniCheck.php new file mode 100644 index 000000000000..0426f9078b77 --- /dev/null +++ b/system/Commands/Utilities/PhpIniCheck.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities; + +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\Security\CheckPhpIni; + +/** + * Check php.ini values. + */ +final class PhpIniCheck extends BaseCommand +{ + /** + * The group the command is lumped under + * when listing commands. + * + * @var string + */ + protected $group = 'CodeIgniter'; + + /** + * The Command's name + * + * @var string + */ + protected $name = 'phpini:check'; + + /** + * The Command's short description + * + * @var string + */ + protected $description = 'Check your php.ini values.'; + + /** + * The Command's usage + * + * @var string + */ + protected $usage = 'phpini:check'; + + /** + * The Command's arguments + * + * @var array + */ + protected $arguments = [ + ]; + + /** + * The Command's options + * + * @var array + */ + protected $options = []; + + /** + * {@inheritDoc} + */ + public function run(array $params) + { + CheckPhpIni::run(); + + return EXIT_SUCCESS; + } +} diff --git a/system/Commands/Utilities/Publish.php b/system/Commands/Utilities/Publish.php index 1e4103c114d8..3e19685cba81 100644 --- a/system/Commands/Utilities/Publish.php +++ b/system/Commands/Utilities/Publish.php @@ -1,5 +1,7 @@ publish()) { CLI::write(lang('Publisher.publishSuccess', [ - get_class($publisher), + $publisher::class, count($publisher->getPublished()), $publisher->getDestination(), ]), 'green'); } else { CLI::error(lang('Publisher.publishFailure', [ - get_class($publisher), + $publisher::class, $publisher->getDestination(), ]), 'light_gray', 'red'); diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 5d20da07d514..f9575c063973 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -1,5 +1,7 @@ getServer(); $_SERVER['HTTP_HOST'] = $host; $request->setGlobal('server', $_SERVER); } - $collection = Services::routes()->loadRoutes(); + $collection = service('routes')->loadRoutes(); // Reset HTTP_HOST if ($host) { unset($_SERVER['HTTP_HOST']); } - $methods = [ - 'get', - 'head', - 'post', - 'patch', - 'put', - 'delete', - 'options', - 'trace', - 'connect', - 'cli', - ]; + $methods = Router::HTTP_METHODS; $tbody = []; $uriGenerator = new SampleURIGenerator(); @@ -172,8 +163,8 @@ public function run(array $params) $autoRoutes = $autoRouteCollector->get(); foreach ($autoRoutes as &$routes) { - // There is no `auto` method, but it is intentional not to get route filters. - $filters = $filterCollector->get('auto', $uriGenerator->get($routes[1])); + // There is no `AUTO` method, but it is intentional not to get route filters. + $filters = $filterCollector->get('AUTO', $uriGenerator->get($routes[1])); $routes[] = implode(' ', array_map('class_basename', $filters['before'])); $routes[] = implode(' ', array_map('class_basename', $filters['after'])); @@ -202,5 +193,30 @@ public function run(array $params) } CLI::table($tbody, $thead); + + $this->showRequiredFilters(); + } + + private function showRequiredFilters(): void + { + $filterCollector = new FilterCollector(); + + $required = $filterCollector->getRequiredFilters(); + + $filters = []; + + foreach ($required['before'] as $filter) { + $filters[] = CLI::color($filter, 'yellow'); + } + + CLI::write('Required Before Filters: ' . implode(', ', $filters)); + + $filters = []; + + foreach ($required['after'] as $filter) { + $filters[] = CLI::color($filter, 'yellow'); + } + + CLI::write(' Required After Filters: ' . implode(', ', $filters)); } } diff --git a/system/Commands/Utilities/Routes/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouteCollector.php index cc45608f21e8..73009b9c9eb8 100644 --- a/system/Commands/Utilities/Routes/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouteCollector.php @@ -1,5 +1,7 @@ namespace = $namespace; - $this->defaultController = $defaultController; - $this->defaultMethod = $defaultMethod; } /** diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index 5a62f8eea8c6..8cbd81fbb02e 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -1,5 +1,7 @@ - */ - private array $protectedControllers; - - /** - * @var string URI prefix for Module Routing - */ - private string $prefix; - - /** - * @param string $namespace namespace to search + * @param string $namespace namespace to search + * @param list $protectedControllers List of controllers in Defined + * Routes that should not be accessed via Auto-Routing. + * @param string $prefix URI prefix for Module Routing */ public function __construct( - string $namespace, - string $defaultController, - string $defaultMethod, - array $httpMethods, - array $protectedControllers, - string $prefix = '' + private readonly string $namespace, + private readonly string $defaultController, + private readonly string $defaultMethod, + private readonly array $httpMethods, + private readonly array $protectedControllers, + private string $prefix = '' ) { - $this->namespace = $namespace; - $this->defaultController = $defaultController; - $this->defaultMethod = $defaultMethod; - $this->httpMethods = $httpMethods; - $this->protectedControllers = $protectedControllers; - $this->prefix = $prefix; } /** diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index a077633f7011..e08a16ff7a60 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -1,5 +1,7 @@ - */ - private array $httpMethods; - - private bool $translateURIDashes; + private readonly bool $translateURIDashes; + private readonly bool $translateUriToCamelCase; /** - * @param string $namespace the default namespace + * @param string $namespace the default namespace + * @param list $httpMethods */ - public function __construct(string $namespace, array $httpMethods) - { - $this->namespace = $namespace; - $this->httpMethods = $httpMethods; - - $config = config(Routing::class); - $this->translateURIDashes = $config->translateURIDashes; + public function __construct( + private readonly string $namespace, + private readonly array $httpMethods + ) { + $config = config(Routing::class); + $this->translateURIDashes = $config->translateURIDashes; + $this->translateUriToCamelCase = $config->translateUriToCamelCase; } /** @@ -65,15 +59,15 @@ public function read(string $class, string $defaultController = 'Home', string $ $classShortname = $reflection->getShortName(); $output = []; - $classInUri = $this->getUriByClass($classname); + $classInUri = $this->convertClassNameToUri($classname); foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { $methodName = $method->getName(); foreach ($this->httpMethods as $httpVerb) { - if (strpos($methodName, $httpVerb) === 0) { + if (str_starts_with($methodName, strtolower($httpVerb))) { // Remove HTTP verb prefix. - $methodInUri = $this->getUriByMethod($httpVerb, $methodName); + $methodInUri = $this->convertMethodNameToUri($httpVerb, $methodName); // Check if it is the default method. if ($methodInUri === $defaultMethod) { @@ -162,7 +156,7 @@ private function getParameters(ReflectionMethod $method): array * * @return string URI path part from the folder(s) and controller */ - private function getUriByClass(string $classname): string + private function convertClassNameToUri(string $classname): string { // remove the namespace $pattern = '/' . preg_quote($this->namespace, '/') . '/'; @@ -179,25 +173,33 @@ private function getUriByClass(string $classname): string $classUri = rtrim($classPath, '/'); - if ($this->translateURIDashes) { - $classUri = str_replace('_', '-', $classUri); - } - - return $classUri; + return $this->translateToUri($classUri); } /** * @return string URI path part from the method */ - private function getUriByMethod(string $httpVerb, string $methodName): string + private function convertMethodNameToUri(string $httpVerb, string $methodName): string { $methodUri = lcfirst(substr($methodName, strlen($httpVerb))); - if ($this->translateURIDashes) { - $methodUri = str_replace('_', '-', $methodUri); + return $this->translateToUri($methodUri); + } + + /** + * @param string $string classname or method name + */ + private function translateToUri(string $string): string + { + if ($this->translateUriToCamelCase) { + $string = strtolower( + preg_replace('/([a-z\d])([A-Z])/', '$1-$2', $string) + ); + } elseif ($this->translateURIDashes) { + $string = str_replace('_', '-', $string); } - return $methodUri; + return $string; } /** diff --git a/system/Commands/Utilities/Routes/ControllerFinder.php b/system/Commands/Utilities/Routes/ControllerFinder.php index f11076cf5fff..71de1681b547 100644 --- a/system/Commands/Utilities/Routes/ControllerFinder.php +++ b/system/Commands/Utilities/Routes/ControllerFinder.php @@ -1,5 +1,7 @@ namespace = $namespace; - $this->locator = Services::locator(); + public function __construct( + private readonly string $namespace + ) { + $this->locator = service('locator'); } /** diff --git a/system/Commands/Utilities/Routes/ControllerMethodReader.php b/system/Commands/Utilities/Routes/ControllerMethodReader.php index 4a37b9560324..c443b669465a 100644 --- a/system/Commands/Utilities/Routes/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/ControllerMethodReader.php @@ -1,5 +1,7 @@ namespace = $namespace; } /** diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php index 01d228a41704..002a529b953e 100644 --- a/system/Commands/Utilities/Routes/FilterCollector.php +++ b/system/Commands/Utilities/Routes/FilterCollector.php @@ -1,5 +1,7 @@ resetRoutes = $resetRoutes; + public function __construct( + /** + * Whether to reset Defined Routes. + * + * If set to true, route filters are not found. + */ + private readonly bool $resetRoutes = false + ) { } /** - * @param string $method HTTP method + * Returns filters for the URI + * + * @param string $method HTTP verb like `GET`,`POST` or `CLI`. * @param string $uri URI path to find filters for * * @return array{before: list, after: list} array of filter alias or classname */ public function get(string $method, string $uri): array { - if ($method === 'cli') { + if ($method === strtolower($method)) { + @trigger_error( + 'Passing lowercase HTTP method "' . $method . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($method) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @deprecated 4.5.0 + * @TODO Remove this in the future. + */ + $method = strtoupper($method); + + if ($method === 'CLI') { return [ 'before' => [], 'after' => [], @@ -62,9 +79,27 @@ public function get(string $method, string $uri): array return $finder->find($uri); } + /** + * Returns Required Filters + * + * @return array{before: list, after: list} array of filter alias or classname + */ + public function getRequiredFilters(): array + { + $request = Services::incomingrequest(null, false); + $request->setMethod(Method::GET); + + $router = $this->createRouter($request); + $filters = $this->createFilters($request); + + $finder = new FilterFinder($router, $filters); + + return $finder->getRequiredFilters(); + } + private function createRouter(Request $request): Router { - $routes = Services::routes(); + $routes = service('routes'); if ($this->resetRoutes) { $routes->resetRoutes(); @@ -77,6 +112,6 @@ private function createFilters(Request $request): Filters { $config = config(FiltersConfig::class); - return new Filters($config, $request, Services::response()); + return new Filters($config, $request, service('response')); } } diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 2e5da617e795..82ea26979517 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -1,5 +1,7 @@ router = $router ?? Services::router(); - $this->filters = $filters ?? Services::filters(); + $this->router = $router ?? service('router'); + $this->filters = $filters ?? service('filters'); } private function getRouteFilters(string $uri): array { $this->router->handle($uri); - $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false; - if (! $multipleFiltersEnabled) { - $filter = $this->router->getFilter(); - - return $filter === null ? [] : [$filter]; - } - return $this->router->getFilters(); } @@ -60,22 +54,45 @@ public function find(string $uri): array // Add route filters try { $routeFilters = $this->getRouteFilters($uri); + $this->filters->enableFilters($routeFilters, 'before'); + + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if (! $oldFilterOrder) { + $routeFilters = array_reverse($routeFilters); + } + $this->filters->enableFilters($routeFilters, 'after'); $this->filters->initialize($uri); return $this->filters->getFilters(); - } catch (RedirectException $e) { + } catch (RedirectException) { return [ 'before' => [], 'after' => [], ]; - } catch (PageNotFoundException $e) { + } catch (PageNotFoundException) { return [ 'before' => [''], 'after' => [''], ]; } } + + /** + * Returns Required Filters + * + * @return array{before: list, after:list} + */ + public function getRequiredFilters(): array + { + [$requiredBefore] = $this->filters->getRequiredFilters('before'); + [$requiredAfter] = $this->filters->getRequiredFilters('after'); + + return [ + 'before' => $requiredBefore, + 'after' => $requiredAfter, + ]; + } } diff --git a/system/Commands/Utilities/Routes/SampleURIGenerator.php b/system/Commands/Utilities/Routes/SampleURIGenerator.php index 43d1934438bb..45eb2f93201b 100644 --- a/system/Commands/Utilities/Routes/SampleURIGenerator.php +++ b/system/Commands/Utilities/Routes/SampleURIGenerator.php @@ -1,5 +1,7 @@ routes = $routes ?? Services::routes(); + $this->routes = $routes ?? service('routes'); } /** @@ -52,7 +53,7 @@ public function get(string $routeKey): string { $sampleUri = $routeKey; - if (strpos($routeKey, '{locale}') !== false) { + if (str_contains($routeKey, '{locale}')) { $sampleUri = str_replace( '{locale}', config(App::class)->defaultLocale, diff --git a/system/Common.php b/system/Common.php index 2ad287fb26ea..f96e9f100f00 100644 --- a/system/Common.php +++ b/system/Common.php @@ -1,5 +1,7 @@ 'APPPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(APPPATH)), + str_starts_with($path, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(SYSTEMPATH)), + str_starts_with($path, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(FCPATH)), + defined('VENDORPATH') && str_starts_with($path, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(VENDORPATH)), + str_starts_with($path, ROOTPATH) => 'ROOTPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(ROOTPATH)), + default => $path, + }; } } @@ -212,6 +203,10 @@ function command(string $command) */ function config(string $name, bool $getShared = true) { + if ($getShared) { + return Factories::get('config', $name); + } + return Factories::config($name, ['getShared' => $getShared]); } } @@ -242,7 +237,7 @@ function cookie(string $name, string $value = '', array $options = []): Cookie function cookies(array $cookies = [], bool $getGlobal = true): CookieStore { if ($getGlobal) { - return Services::response()->getCookieStore(); + return service('response')->getCookieStore(); } return new CookieStore($cookies); @@ -257,7 +252,7 @@ function cookies(array $cookies = [], bool $getGlobal = true): CookieStore */ function csrf_token(): string { - return Services::security()->getTokenName(); + return service('security')->getTokenName(); } } @@ -269,7 +264,7 @@ function csrf_token(): string */ function csrf_header(): string { - return Services::security()->getHeaderName(); + return service('security')->getHeaderName(); } } @@ -281,7 +276,7 @@ function csrf_header(): string */ function csrf_hash(): string { - return Services::security()->getHash(); + return service('security')->getHash(); } } @@ -315,7 +310,7 @@ function csrf_meta(?string $id = null): string */ function csp_style_nonce(): string { - $csp = Services::csp(); + $csp = service('csp'); if (! $csp->enabled()) { return ''; @@ -331,7 +326,7 @@ function csp_style_nonce(): string */ function csp_script_nonce(): string { - $csp = Services::csp(); + $csp = service('csp'); if (! $csp->enabled()) { return ''; @@ -387,21 +382,13 @@ function env(string $key, $default = null) } // Handle any boolean values - switch (strtolower($value)) { - case 'true': - return true; - - case 'false': - return false; - - case 'empty': - return ''; - - case 'null': - return null; - } - - return $value; + return match (strtolower($value)) { + 'true' => true, + 'false' => false, + 'empty' => '', + 'null' => null, + default => $value, + }; } } @@ -484,13 +471,13 @@ function force_https( ?RequestInterface $request = null, ?ResponseInterface $response = null ): void { - $request ??= Services::request(); + $request ??= service('request'); if (! $request instanceof IncomingRequest) { return; } - $response ??= Services::response(); + $response ??= service('response'); if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure())) || $request->getServer('HTTPS') === 'test' @@ -501,7 +488,7 @@ function force_https( // If the session status is active, we should regenerate // the session ID for safety sake. if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) { - Services::session()->regenerate(); // @codeCoverageIgnore + service('session')->regenerate(); // @codeCoverageIgnore } $uri = $request->getUri()->withScheme('https'); @@ -580,7 +567,7 @@ function helper($filenames): void { static $loaded = []; - $loader = Services::locator(); + $loader = service('locator'); if (! is_array($filenames)) { $filenames = [$filenames]; @@ -596,7 +583,7 @@ function helper($filenames): void $appHelper = null; $localIncludes = []; - if (strpos($filename, '_helper') === false) { + if (! str_contains($filename, '_helper')) { $filename .= '_helper'; } @@ -607,7 +594,7 @@ function helper($filenames): void // If the file is namespaced, we'll just grab that // file and not search for any others - if (strpos($filename, '\\') !== false) { + if (str_contains($filename, '\\')) { $path = $loader->locateFile($filename, 'Helpers'); if (empty($path)) { @@ -621,9 +608,9 @@ function helper($filenames): void $paths = $loader->search('Helpers/' . $filename); foreach ($paths as $path) { - if (strpos($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) { + if (str_starts_with($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR)) { $appHelper = $path; - } elseif (strpos($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR) === 0) { + } elseif (str_starts_with($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR)) { $systemHelper = $path; } else { $localIncludes[] = $path; @@ -745,7 +732,7 @@ function is_windows(?bool $mock = null): bool */ function lang(string $line, array $args = [], ?string $locale = null) { - $language = Services::language(); + $language = service('language'); // Get active locale $activeLocale = $language->getLocale(); @@ -754,14 +741,14 @@ function lang(string $line, array $args = [], ?string $locale = null) $language->setLocale($locale); } - $line = $language->getLine($line, $args); + $lines = $language->getLine($line, $args); if ($locale && $locale !== $activeLocale) { // Reset to active locale $language->setLocale($activeLocale); } - return $line; + return $lines; } } @@ -780,7 +767,7 @@ function lang(string $line, array $args = [], ?string $locale = null) * - info * - debug * - * @return bool + * @return void */ function log_message(string $level, string $message, array $context = []) { @@ -790,10 +777,12 @@ function log_message(string $level, string $message, array $context = []) if (ENVIRONMENT === 'testing') { $logger = new TestLogger(new Logger()); - return $logger->log($level, $message, $context); + $logger->log($level, $message, $context); + + return; } - return Services::logger(true)->log($level, $message, $context); // @codeCoverageIgnore + service('logger')->log($level, $message, $context); // @codeCoverageIgnore } } @@ -832,7 +821,7 @@ function old(string $key, $default = null, $escape = 'html') session(); // @codeCoverageIgnore } - $request = Services::request(); + $request = service('request'); $value = $request->getOldInput($key); @@ -858,7 +847,7 @@ function old(string $key, $default = null, $escape = 'html') */ function redirect(?string $route = null): RedirectResponse { - $response = Services::redirectresponse(null, true); + $response = service('redirectresponse'); if ($route !== null) { return $response->route($route); @@ -930,7 +919,7 @@ function remove_invisible_characters(string $str, bool $urlEncoded = true): stri */ function request() { - return Services::request(); + return service('request'); } } @@ -940,7 +929,7 @@ function request() */ function response(): ResponseInterface { - return Services::response(); + return service('response'); } } @@ -961,7 +950,7 @@ function response(): ResponseInterface */ function route_to(string $method, ...$params) { - return Services::routes()->reverseRoute($method, ...$params); + return service('routes')->reverseRoute($method, ...$params); } } @@ -979,7 +968,7 @@ function route_to(string $method, ...$params) */ function session(?string $val = null) { - $session = Services::session(); + $session = service('session'); // Returning a single item? if (is_string($val)) { @@ -1005,6 +994,10 @@ function session(?string $val = null) */ function service(string $name, ...$params): ?object { + if ($params === []) { + return Services::get($name); + } + return Services::$name(...$params); } } @@ -1133,7 +1126,7 @@ function stringify_attributes($attributes, bool $js = false): string */ function timer(?string $name = null, ?callable $callable = null) { - $timer = Services::timer(); + $timer = service('timer'); if ($name === null) { return $timer; @@ -1165,7 +1158,7 @@ function timer(?string $name = null, ?callable $callable = null) */ function view(string $name, array $data = [], array $options = []): string { - $renderer = Services::renderer(); + $renderer = service('renderer'); $config = config(View::class); $saveData = $config->saveData; @@ -1190,7 +1183,7 @@ function view(string $name, array $data = [], array $options = []): string */ function view_cell(string $library, $params = null, int $ttl = 0, ?string $cacheName = null): string { - return Services::viewcell() + return service('viewcell') ->render($library, $params, $ttl, $cacheName); } } @@ -1213,7 +1206,7 @@ function view_cell(string $library, $params = null, int $ttl = 0, ?string $cache */ function class_basename($class) { - $class = is_object($class) ? get_class($class) : $class; + $class = is_object($class) ? $class::class : $class; return basename(str_replace('\\', '/', $class)); } @@ -1232,7 +1225,7 @@ function class_basename($class) function class_uses_recursive($class) { if (is_object($class)) { - $class = get_class($class); + $class = $class::class; } $results = []; diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php index b95bdf54c4ae..661247dab532 100644 --- a/system/ComposerScripts.php +++ b/system/ComposerScripts.php @@ -1,5 +1,7 @@ [ 'license' => __DIR__ . '/../vendor/psr/log/LICENSE', - 'from' => __DIR__ . '/../vendor/psr/log/Psr/Log/', + 'from' => __DIR__ . '/../vendor/psr/log/src/', 'to' => __DIR__ . '/ThirdParty/PSR/Log/', ], ]; @@ -71,7 +73,7 @@ public static function postUpdate() foreach (self::$dependencies as $key => $dependency) { // Kint may be removed. - if (! is_dir($dependency['from']) && strpos($key, 'kint') === 0) { + if (! is_dir($dependency['from']) && str_starts_with($key, 'kint')) { continue; } @@ -84,7 +86,6 @@ public static function postUpdate() } self::copyKintInitFiles(); - self::recursiveDelete(self::$dependencies['psr-log']['to'] . 'Test/'); } /** diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 33c977cc0bb6..0a99cdb0bb28 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -1,5 +1,7 @@ SYSTEMPATH, - 'App' => APPPATH, // To ensure filters, etc still found, + 'Config' => APPPATH . 'Config', ]; /** @@ -103,7 +105,7 @@ class AutoloadConfig * searched for within one or more directories as they would if they * were being autoloaded through a namespace. * - * @var array + * @var array */ protected $coreClassmap = [ AbstractLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php', diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 5dd6cde2ad02..8a82cbf7ce64 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -13,7 +13,6 @@ use Config\Encryption; use Config\Modules; -use Config\Services; use ReflectionClass; use ReflectionException; use RuntimeException; @@ -120,10 +119,10 @@ public function __construct() $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix); if ($this instanceof Encryption && $property === 'key') { - if (strpos($this->{$property}, 'hex2bin:') === 0) { + if (str_starts_with($this->{$property}, 'hex2bin:')) { // Handle hex2bin prefix $this->{$property} = hex2bin(substr($this->{$property}, 8)); - } elseif (strpos($this->{$property}, 'base64:') === 0) { + } elseif (str_starts_with($this->{$property}, 'base64:')) { // Handle base64 prefix $this->{$property} = base64_decode(substr($this->{$property}, 7), true); } @@ -164,6 +163,9 @@ protected function initEnvValue(&$property, string $name, string $prefix, string $value = (float) $value; } + // If the default value of the property is `null` and the type is not + // `string`, TypeError will happen. + // So cannot set `declare(strict_types=1)` in this file. $property = $value; } } @@ -228,11 +230,16 @@ protected function registerProperties() } if (! static::$didDiscovery) { - $locator = Services::locator(); + $locator = service('locator'); $registrarsFiles = $locator->search('Config/Registrar.php'); foreach ($registrarsFiles as $file) { - $className = $locator->getClassname($file); + $className = $locator->findQualifiedNameFromPath($file); + + if ($className === false) { + continue; + } + static::$registrars[] = new $className(); } diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 835145eee460..2f8df2a35420 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -1,5 +1,7 @@ [key => instance] */ protected static $instances = []; + /** + * Factory method list. + * + * @var array [key => callable] + */ + protected static array $factories = []; + /** * Mock objects for testing which are returned if exist. * - * @var array + * @var array [key => instance] */ protected static $mocks = []; @@ -164,6 +177,8 @@ class BaseService * A cache of other service classes we've found. * * @var array + * + * @deprecated 4.5.0 No longer used. */ protected static $services = []; @@ -174,6 +189,42 @@ class BaseService */ private static array $serviceNames = []; + /** + * Simple method to get an entry fast. + * + * @param string $key Identifier of the entry to look for. + * + * @return object|null Entry. + */ + public static function get(string $key): ?object + { + return static::$instances[$key] ?? static::__callStatic($key, []); + } + + /** + * Sets an entry. + * + * @param string $key Identifier of the entry. + */ + public static function set(string $key, object $value): void + { + if (isset(static::$instances[$key])) { + throw new InvalidArgumentException('The entry for "' . $key . '" is already set.'); + } + + static::$instances[$key] = $value; + } + + /** + * Overrides an existing entry. + * + * @param string $key Identifier of the entry. + */ + public static function override(string $key, object $value): void + { + static::$instances[$key] = $value; + } + /** * Returns a shared instance of any of the class' services. * @@ -226,13 +277,20 @@ public static function autoloader(bool $getShared = true) * within namespaced folders, as well as convenience methods for * loading 'helpers', and 'libraries'. * - * @return FileLocator + * @return FileLocatorInterface */ public static function locator(bool $getShared = true) { if ($getShared) { if (empty(static::$instances['locator'])) { - static::$instances['locator'] = new FileLocator(static::autoloader()); + $cacheEnabled = class_exists(Optimize::class) + && (new Optimize())->locatorCacheEnabled; + + if ($cacheEnabled) { + static::$instances['locator'] = new FileLocatorCached(new FileLocator(static::autoloader())); + } else { + static::$instances['locator'] = new FileLocator(static::autoloader()); + } } return static::$mocks['locator'] ?? static::$instances['locator']; @@ -249,6 +307,10 @@ public static function locator(bool $getShared = true) */ public static function __callStatic(string $name, array $arguments) { + if (isset(static::$factories[$name])) { + return static::$factories[$name](...$arguments); + } + $service = static::serviceExists($name); if ($service === null) { @@ -265,11 +327,14 @@ public static function __callStatic(string $name, array $arguments) public static function serviceExists(string $name): ?string { static::buildServicesCache(); + $services = array_merge(self::$serviceNames, [Services::class]); $name = strtolower($name); foreach ($services as $service) { if (method_exists($service, $name)) { + static::$factories[$name] = [$service, $name]; + return $service; } } @@ -286,6 +351,7 @@ public static function reset(bool $initAutoloader = true) { static::$mocks = []; static::$instances = []; + static::$factories = []; if ($initAutoloader) { static::autoloader()->initialize(new Autoload(), new Modules()); @@ -312,75 +378,27 @@ public static function resetSingle(string $name) */ public static function injectMock(string $name, $mock) { + static::$instances[$name] = $mock; static::$mocks[strtolower($name)] = $mock; } - /** - * Will scan all psr4 namespaces registered with system to look - * for new Config\Services files. Caches a copy of each one, then - * looks for the service method in each, returning an instance of - * the service, if available. - * - * @return object|null - * - * @deprecated - * - * @codeCoverageIgnore - */ - protected static function discoverServices(string $name, array $arguments) + protected static function buildServicesCache(): void { if (! static::$discovered) { if ((new Modules())->shouldDiscover('services')) { $locator = static::locator(); $files = $locator->search('Config/Services'); - if (empty($files)) { - // no files at all found - this would be really, really bad - return null; - } - // Get instances of all service classes and cache them locally. foreach ($files as $file) { - $classname = $locator->getClassname($file); + $classname = $locator->findQualifiedNameFromPath($file); - if ($classname !== Services::class) { - static::$services[] = new $classname(); + if ($classname === false) { + continue; } - } - } - - static::$discovered = true; - } - - if (! static::$services) { - // we found stuff, but no services - this would be really bad - return null; - } - - // Try to find the desired service method - foreach (static::$services as $class) { - if (method_exists($class, $name)) { - return $class::$name(...$arguments); - } - } - - return null; - } - - protected static function buildServicesCache(): void - { - if (! static::$discovered) { - if ((new Modules())->shouldDiscover('services')) { - $locator = static::locator(); - $files = $locator->search('Config/Services'); - - // Get instances of all service classes and cache them locally. - foreach ($files as $file) { - $classname = $locator->getClassname($file); if ($classname !== Services::class) { self::$serviceNames[] = $classname; - static::$services[] = new $classname(); } } } diff --git a/system/Config/Config.php b/system/Config/Config.php deleted file mode 100644 index 90a6b7cadef5..000000000000 --- a/system/Config/Config.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Config; - -/** - * @deprecated Use CodeIgniter\Config\Factories::config() - * @see \CodeIgniter\Config\ConfigTest - */ -class Config -{ - /** - * Create new configuration instances or return - * a shared instance - * - * @param string $name Configuration name - * @param bool $getShared Use shared instance - * - * @return object|null - */ - public static function get(string $name, bool $getShared = true) - { - return Factories::config($name, ['getShared' => $getShared]); - } - - /** - * Helper method for injecting mock instances while testing. - * - * @param object $instance - * - * @return void - */ - public static function injectMock(string $name, $instance) - { - Factories::injectMock('config', $name, $instance); - } - - /** - * Resets the static arrays - * - * @return void - */ - public static function reset() - { - Factories::reset('config'); - } -} diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index f8e757e9eab5..db7152fd09d5 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -1,5 +1,7 @@ normaliseVariable($line); $vars[$name] = $value; $this->setVariable($name, $value); @@ -112,7 +114,7 @@ protected function setVariable(string $name, string $value = '') public function normaliseVariable(string $name, string $value = ''): array { // Split our compound string into its parts. - if (strpos($name, '=') !== false) { + if (str_contains($name, '=')) { [$name, $value] = explode('=', $name, 2); } @@ -192,7 +194,7 @@ protected function sanitizeValue(string $value): string */ protected function resolveNestedVariables(string $value): string { - if (strpos($value, '$') !== false) { + if (str_contains($value, '$')) { $value = preg_replace_callback( '/\${([a-zA-Z0-9_\.]+)}/', function ($matchedPatterns) { diff --git a/system/Config/Factories.php b/system/Config/Factories.php index c86bfd95cc5d..f8102118bc1d 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -1,5 +1,7 @@ > */ - protected static $options = []; + private static $options = []; /** * Explicit options for the Config @@ -64,7 +65,7 @@ class Factories * * @var array> */ - protected static $aliases = []; + private static $aliases = []; /** * Store for instances of any component that @@ -77,7 +78,7 @@ class Factories * * @var array> */ - protected static $instances = []; + private static $instances = []; /** * Whether the component instances are updated? @@ -86,7 +87,7 @@ class Factories * * @internal For caching only */ - protected static $updated = []; + private static $updated = []; /** * Define the class to load. You can *override* the concrete class. @@ -139,8 +140,8 @@ public static function __callStatic(string $component, array $arguments) $options = array_merge(self::getOptions($component), $options); if (! $options['getShared']) { - if (isset(self::$aliases[$component][$alias])) { - $class = self::$aliases[$component][$alias]; + if (isset(self::$aliases[$options['component']][$alias])) { + $class = self::$aliases[$options['component']][$alias]; return new $class(...$arguments); } @@ -171,6 +172,20 @@ public static function __callStatic(string $component, array $arguments) return self::$instances[$options['component']][$class]; } + /** + * Simple method to get the shared instance fast. + */ + public static function get(string $component, string $alias): ?object + { + if (isset(self::$aliases[$component][$alias])) { + $class = self::$aliases[$component][$alias]; + + return self::$instances[$component][$class]; + } + + return self::__callStatic($component, [$alias]); + } + /** * Gets the defined instance. If not exists, creates new one. * @@ -249,7 +264,7 @@ private static function isConfig(string $component): bool * @param array $options The array of component-specific directives * @param string $alias Class alias. See the $aliases property. */ - protected static function locateClass(array $options, string $alias): ?string + private static function locateClass(array $options, string $alias): ?string { // Check for low-hanging fruit if ( @@ -282,7 +297,7 @@ class_exists($alias, false) } // Have to do this the hard way... - $locator = Services::locator(); + $locator = service('locator'); // Check if the class alias was namespaced if (self::isNamespaced($alias)) { @@ -299,9 +314,9 @@ class_exists($alias, false) // Check all files for a valid class foreach ($files as $file) { - $class = $locator->getClassname($file); + $class = $locator->findQualifiedNameFromPath($file); - if ($class && self::verifyInstanceOf($options, $class)) { + if ($class !== false && self::verifyInstanceOf($options, $class)) { return $class; } } @@ -316,7 +331,7 @@ class_exists($alias, false) */ private static function isNamespaced(string $alias): bool { - return strpos($alias, '\\') !== false; + return str_contains($alias, '\\'); } /** @@ -325,7 +340,7 @@ private static function isNamespaced(string $alias): bool * @param array $options The array of component-specific directives * @param string $alias Class alias. See the $aliases property. */ - protected static function verifyPreferApp(array $options, string $alias): bool + private static function verifyPreferApp(array $options, string $alias): bool { // Anything without that restriction passes if (! $options['preferApp']) { @@ -334,10 +349,10 @@ protected static function verifyPreferApp(array $options, string $alias): bool // Special case for Config since its App namespace is actually \Config if (self::isConfig($options['component'])) { - return strpos($alias, 'Config') === 0; + return str_starts_with($alias, 'Config'); } - return strpos($alias, APP_NAMESPACE) === 0; + return str_starts_with($alias, APP_NAMESPACE); } /** @@ -346,7 +361,7 @@ protected static function verifyPreferApp(array $options, string $alias): bool * @param array $options The array of component-specific directives * @param string $alias Class alias. See the $aliases property. */ - protected static function verifyInstanceOf(array $options, string $alias): bool + private static function verifyInstanceOf(array $options, string $alias): bool { // Anything without that restriction passes if (! $options['instanceOf']) { @@ -461,7 +476,7 @@ public static function injectMock(string $component, string $alias, object $inst // Force a configuration to exist for this component self::getOptions($component); - $class = get_class($instance); + $class = $instance::class; self::$instances[$component][$class] = $instance; self::$aliases[$component][$alias] = $class; diff --git a/system/Config/Factory.php b/system/Config/Factory.php index 5889bf4d7336..b25267791beb 100644 --- a/system/Config/Factory.php +++ b/system/Config/Factory.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Config; + +use CodeIgniter\Filters\Cors; +use CodeIgniter\Filters\CSRF; +use CodeIgniter\Filters\DebugToolbar; +use CodeIgniter\Filters\ForceHTTPS; +use CodeIgniter\Filters\Honeypot; +use CodeIgniter\Filters\InvalidChars; +use CodeIgniter\Filters\PageCache; +use CodeIgniter\Filters\PerformanceMetrics; +use CodeIgniter\Filters\SecureHeaders; + +/** + * Filters configuration + */ +class Filters extends BaseConfig +{ + /** + * Configures aliases for Filter classes to + * make reading things nicer and simpler. + * + * @var array> + * + * [filter_name => classname] + * or [filter_name => [classname1, classname2, ...]] + */ + public array $aliases = [ + 'csrf' => CSRF::class, + 'toolbar' => DebugToolbar::class, + 'honeypot' => Honeypot::class, + 'invalidchars' => InvalidChars::class, + 'secureheaders' => SecureHeaders::class, + 'cors' => Cors::class, + 'forcehttps' => ForceHTTPS::class, + 'pagecache' => PageCache::class, + 'performance' => PerformanceMetrics::class, + ]; + + /** + * List of special required filters. + * + * The filters listed here are special. They are applied before and after + * other kinds of filters, and always applied even if a route does not exist. + * + * Filters set by default provide framework functionality. If removed, + * those functions will no longer work. + * + * @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters + * + * @var array{before: list, after: list} + */ + public array $required = [ + 'before' => [ + 'forcehttps', // Force Global Secure Requests + 'pagecache', // Web Page Caching + ], + 'after' => [ + 'pagecache', // Web Page Caching + 'performance', // Performance Metrics + 'toolbar', // Debug Toolbar + ], + ]; + + /** + * List of filter aliases that are always + * applied before and after every request. + * + * @var array>>|array> + */ + public array $globals = [ + 'before' => [ + // 'honeypot', + // 'csrf', + // 'invalidchars', + ], + 'after' => [ + // 'honeypot', + // 'secureheaders', + ], + ]; + + /** + * List of filter aliases that works on a + * particular HTTP method (GET, POST, etc.). + * + * Example: + * 'POST' => ['foo', 'bar'] + * + * If you use this, you should disable auto-routing because auto-routing + * permits any HTTP method to access a controller. Accessing the controller + * with a method you don't expect could bypass the filter. + */ + public array $methods = []; + + /** + * List of filter aliases that should run on any + * before or after URI patterns. + * + * Example: + * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']] + */ + public array $filters = []; +} diff --git a/system/Config/ForeignCharacters.php b/system/Config/ForeignCharacters.php index a8569f5419c5..2663a53a82ed 100644 --- a/system/Config/ForeignCharacters.php +++ b/system/Config/ForeignCharacters.php @@ -1,5 +1,7 @@ */ public array $moduleRoutes = []; + + /** + * For Auto Routing (Improved). + * Whether to translate dashes in URIs for controller/method to CamelCase. + * E.g., blog-controller -> BlogController + * + * If you enable this, $translateURIDashes is ignored. + * + * Default: false + */ + public bool $translateUriToCamelCase = false; } diff --git a/system/Config/Services.php b/system/Config/Services.php index 738809b44595..422799e0b87c 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -1,5 +1,7 @@ withFile($path)->rotate(90)->save(); + * of the handler. Used like service('image')->withFile($path)->rotate(90)->save(); * * @return BaseHandler */ @@ -374,8 +376,8 @@ public static function language(?string $locale = null, bool $getShared = true) return static::getSharedInstance('language', $locale)->setLocale($locale); } - if (AppServices::request() instanceof IncomingRequest) { - $requestLocale = AppServices::request()->getLocale(); + if (AppServices::get('request') instanceof IncomingRequest) { + $requestLocale = AppServices::get('request')->getLocale(); } else { $requestLocale = Locale::getDefault(); } @@ -430,7 +432,7 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh return static::getSharedInstance('negotiator', $request); } - $request ??= AppServices::request(); + $request ??= AppServices::get('request'); return new Negotiate($request); } @@ -447,7 +449,7 @@ public static function responsecache(?Cache $config = null, ?CacheInterface $cac } $config ??= config(Cache::class); - $cache ??= AppServices::cache(); + $cache ??= AppServices::get('cache'); return new ResponseCache($config, $cache); } @@ -483,7 +485,7 @@ public static function parser(?string $viewPath = null, ?ViewConfig $config = nu $viewPath = $viewPath ?: (new Paths())->viewDirectory; $config ??= config(ViewConfig::class); - return new Parser($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger()); + return new Parser($config, $viewPath, AppServices::get('locator'), CI_DEBUG, AppServices::get('logger')); } /** @@ -502,7 +504,7 @@ public static function renderer(?string $viewPath = null, ?ViewConfig $config = $viewPath = $viewPath ?: (new Paths())->viewDirectory; $config ??= config(ViewConfig::class); - return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger()); + return new View($config, $viewPath, AppServices::get('locator'), CI_DEBUG, AppServices::get('logger')); } /** @@ -542,7 +544,7 @@ public static function createRequest(App $config, bool $isCli = false): void $request->setProtocolVersion($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'); } - // Inject the request object into Services::request(). + // Inject the request object into Services. static::$instances['request'] = $request; } @@ -563,7 +565,7 @@ public static function incomingrequest(?App $config = null, bool $getShared = tr return new IncomingRequest( $config, - AppServices::uri(), + AppServices::get('uri'), 'php://input', new UserAgent() ); @@ -598,7 +600,7 @@ public static function redirectresponse(?App $config = null, bool $getShared = t $config ??= config(App::class); $response = new RedirectResponse($config); - $response->setProtocolVersion(AppServices::request()->getProtocolVersion()); + $response->setProtocolVersion(AppServices::get('request')->getProtocolVersion()); return $response; } @@ -615,7 +617,7 @@ public static function routes(bool $getShared = true) return static::getSharedInstance('routes'); } - return new RouteCollection(AppServices::locator(), config(Modules::class), config(Routing::class)); + return new RouteCollection(AppServices::get('locator'), config(Modules::class), config(Routing::class)); } /** @@ -630,8 +632,8 @@ public static function router(?RouteCollectionInterface $routes = null, ?Request return static::getSharedInstance('router', $routes, $request); } - $routes ??= AppServices::routes(); - $request ??= AppServices::request(); + $routes ??= AppServices::get('routes'); + $request ??= AppServices::get('request'); return new Router($routes, $request); } @@ -666,7 +668,7 @@ public static function session(?SessionConfig $config = null, bool $getShared = $config ??= config(SessionConfig::class); - $logger = AppServices::logger(); + $logger = AppServices::get('logger'); $driverName = $config->driver; @@ -683,7 +685,7 @@ public static function session(?SessionConfig $config = null, bool $getShared = } } - $driver = new $driverName($config, AppServices::request()->getIPAddress()); + $driver = new $driverName($config, AppServices::get('request')->getIPAddress()); $driver->setLogger($logger); $session = new Session($driver, $config); @@ -717,7 +719,7 @@ public static function siteurifactory( } $config ??= config('App'); - $superglobals ??= AppServices::superglobals(); + $superglobals ??= AppServices::get('superglobals'); return new SiteURIFactory($config, $superglobals); } @@ -751,7 +753,7 @@ public static function throttler(bool $getShared = true) return static::getSharedInstance('throttler'); } - return new Throttler(AppServices::cache()); + return new Throttler(AppServices::get('cache')); } /** @@ -800,7 +802,7 @@ public static function uri(?string $uri = null, bool $getShared = true) if ($uri === null) { $appConfig = config(App::class); - $factory = AppServices::siteurifactory($appConfig, AppServices::superglobals()); + $factory = AppServices::siteurifactory($appConfig, AppServices::get('superglobals')); return $factory->createFromGlobals(); } @@ -821,7 +823,7 @@ public static function validation(?ValidationConfig $config = null, bool $getSha $config ??= config(ValidationConfig::class); - return new Validation($config, AppServices::renderer()); + return new Validation($config, AppServices::get('renderer')); } /** @@ -836,7 +838,7 @@ public static function viewcell(bool $getShared = true) return static::getSharedInstance('viewcell'); } - return new Cell(AppServices::cache()); + return new Cell(AppServices::get('cache')); } /** diff --git a/system/Config/View.php b/system/Config/View.php index ea1442e5bc62..038c6fa774dc 100644 --- a/system/Config/View.php +++ b/system/Config/View.php @@ -1,5 +1,7 @@ setTtl($time); - } - - /** - * Handles "auto-loading" helper files. - * - * @deprecated Use `helper` function instead of using this method. - * - * @codeCoverageIgnore - * - * @return void - */ - protected function loadHelpers() - { - if ($this->helpers === []) { - return; - } - - helper($this->helpers); + service('responsecache')->setTtl($time); } /** @@ -174,7 +157,7 @@ protected function validateData(array $data, $rules, array $messages = [], ?stri */ private function setValidator($rules, array $messages): void { - $this->validator = Services::validation(); + $this->validator = service('validation'); // If you replace the $rules array with the name of the group if (is_string($rules)) { diff --git a/system/Cookie/CloneableCookieInterface.php b/system/Cookie/CloneableCookieInterface.php index 93f6031e2010..0b7d6fdf5015 100644 --- a/system/Cookie/CloneableCookieInterface.php +++ b/system/Cookie/CloneableCookieInterface.php @@ -1,5 +1,7 @@ $cookie) { - $type = is_object($cookie) ? get_class($cookie) : gettype($cookie); + $type = get_debug_type($cookie); if (! $cookie instanceof Cookie) { throw CookieException::forInvalidCookieInstance([static::class, Cookie::class, $type, $index]); diff --git a/system/Cookie/Exceptions/CookieException.php b/system/Cookie/Exceptions/CookieException.php index af466061155b..8b6c7575983a 100644 --- a/system/Cookie/Exceptions/CookieException.php +++ b/system/Cookie/Exceptions/CookieException.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +/** + * Class ArrayCast + * + * (PHP) [array --> string] --> (DB driver) --> (DB column) string + * [ <-- string] <-- (DB driver) <-- (DB column) string + */ +class ArrayCast extends BaseCast implements CastInterface +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): array { + if (! is_string($value)) { + self::invalidTypeValueError($value); + } + + if ((str_starts_with($value, 'a:') || str_starts_with($value, 's:'))) { + $value = unserialize($value, ['allowed_classes' => false]); + } + + return (array) $value; + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { + return serialize($value); + } +} diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php new file mode 100644 index 000000000000..c3df0efee103 --- /dev/null +++ b/system/DataCaster/Cast/BaseCast.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +use InvalidArgumentException; + +abstract class BaseCast implements CastInterface +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed { + return $value; + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed { + return $value; + } + + protected static function invalidTypeValueError(mixed $value): never + { + $message = '[' . static::class . '] Invalid value type: ' . get_debug_type($value); + if (is_scalar($value)) { + $message .= ', and its value: ' . var_export($value, true); + } + + throw new InvalidArgumentException($message); + } +} diff --git a/system/DataCaster/Cast/BooleanCast.php b/system/DataCaster/Cast/BooleanCast.php new file mode 100644 index 000000000000..e4a3fde09345 --- /dev/null +++ b/system/DataCaster/Cast/BooleanCast.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +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) + */ +class BooleanCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): bool { + // For PostgreSQL + if ($value === 't') { + return true; + } + if ($value === 'f') { + return false; + } + + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } +} diff --git a/system/DataCaster/Cast/CSVCast.php b/system/DataCaster/Cast/CSVCast.php new file mode 100644 index 000000000000..42dd3709a1db --- /dev/null +++ b/system/DataCaster/Cast/CSVCast.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +/** + * Class CSVCast + * + * (PHP) [array --> string] --> (DB driver) --> (DB column) string + * [ <-- string] <-- (DB driver) <-- (DB column) string + */ +class CSVCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): array { + if (! is_string($value)) { + self::invalidTypeValueError($value); + } + + return explode(',', $value); + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { + if (! is_array($value)) { + self::invalidTypeValueError($value); + } + + return implode(',', $value); + } +} diff --git a/system/DataCaster/Cast/CastInterface.php b/system/DataCaster/Cast/CastInterface.php new file mode 100644 index 000000000000..ff93dc2860bc --- /dev/null +++ b/system/DataCaster/Cast/CastInterface.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +interface CastInterface +{ + /** + * Takes a value from DataSource, returns its value for PHP. + * + * @param mixed $value Data from database driver + * @param list $params Additional param + * @param object|null $helper Helper object. E.g., database connection + * + * @return mixed PHP native value + */ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed; + + /** + * Takes a PHP value, returns its value for DataSource. + * + * @param mixed $value PHP native value + * @param list $params Additional param + * @param object|null $helper Helper object. E.g., database connection + * + * @return mixed Data to pass to database driver + */ + 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 new file mode 100644 index 000000000000..83e66d02217c --- /dev/null +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +use CodeIgniter\Database\BaseConnection; +use CodeIgniter\I18n\Time; +use InvalidArgumentException; + +/** + * Class DatetimeCast + * + * (PHP) [Time --> string] --> (DB driver) --> (DB column) datetime + * [ <-- string] <-- (DB driver) <-- (DB column) datetime + */ +class DatetimeCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): Time { + if (! is_string($value)) { + self::invalidTypeValueError($value); + } + + if (! $helper instanceof BaseConnection) { + $message = 'The parameter $helper must be BaseConnection.'; + + throw new InvalidArgumentException($message); + } + + /** + * @see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters + */ + $format = match ($params[0] ?? '') { + '' => $helper->dateFormat['datetime'], + 'ms' => $helper->dateFormat['datetime-ms'], + 'us' => $helper->dateFormat['datetime-us'], + default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]), + }; + + return Time::createFromFormat($format, $value); + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { + if (! $value instanceof Time) { + self::invalidTypeValueError($value); + } + + return (string) $value; + } +} diff --git a/system/DataCaster/Cast/FloatCast.php b/system/DataCaster/Cast/FloatCast.php new file mode 100644 index 000000000000..7ced2e2653a7 --- /dev/null +++ b/system/DataCaster/Cast/FloatCast.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +/** + * Class FloatCast + * + * (PHP) [float --> float ] --> (DB driver) --> (DB column) float + * [ <-- float|string] <-- (DB driver) <-- (DB column) float + */ +class FloatCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): float { + if (! is_float($value) && ! is_string($value)) { + self::invalidTypeValueError($value); + } + + return (float) $value; + } +} diff --git a/system/DataCaster/Cast/IntBoolCast.php b/system/DataCaster/Cast/IntBoolCast.php new file mode 100644 index 000000000000..56977c842f0e --- /dev/null +++ b/system/DataCaster/Cast/IntBoolCast.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +/** + * Int Bool Cast + * + * (PHP) [bool --> int ] --> (DB driver) --> (DB column) int(0/1) + * [ <-- int|string] <-- (DB driver) <-- (DB column) int(0/1) + */ +final class IntBoolCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): bool { + if (! is_int($value) && ! is_string($value)) { + self::invalidTypeValueError($value); + } + + return (bool) $value; + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): int { + if (! is_bool($value)) { + self::invalidTypeValueError($value); + } + + return (int) $value; + } +} diff --git a/system/DataCaster/Cast/IntegerCast.php b/system/DataCaster/Cast/IntegerCast.php new file mode 100644 index 000000000000..e16683b1fb8f --- /dev/null +++ b/system/DataCaster/Cast/IntegerCast.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +/** + * Class IntegerCast + * + * (PHP) [int --> int ] --> (DB driver) --> (DB column) int + * [ <-- int|string] <-- (DB driver) <-- (DB column) int + */ +class IntegerCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): int { + if (! is_string($value) && ! is_int($value)) { + self::invalidTypeValueError($value); + } + + return (int) $value; + } +} diff --git a/system/DataCaster/Cast/JsonCast.php b/system/DataCaster/Cast/JsonCast.php new file mode 100644 index 000000000000..316070aaedee --- /dev/null +++ b/system/DataCaster/Cast/JsonCast.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +use CodeIgniter\DataCaster\Exceptions\CastException; +use JsonException; +use stdClass; + +/** + * Class JsonCast + * + * (PHP) [array|stdClass --> string] --> (DB driver) --> (DB column) string + * [ <-- string] <-- (DB driver) <-- (DB column) string + */ +class JsonCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): array|stdClass { + if (! is_string($value)) { + self::invalidTypeValueError($value); + } + + $associative = in_array('array', $params, true); + + $output = ($associative ? [] : new stdClass()); + + try { + $output = json_decode($value, $associative, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw CastException::forInvalidJsonFormat($e->getCode()); + } + + return $output; + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { + try { + $output = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw CastException::forInvalidJsonFormat($e->getCode()); + } + + return $output; + } +} diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php new file mode 100644 index 000000000000..52a4d88f9e46 --- /dev/null +++ b/system/DataCaster/Cast/TimestampCast.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +use CodeIgniter\I18n\Time; + +/** + * Class TimestampCast + * + * (PHP) [Time --> int ] --> (DB driver) --> (DB column) int + * [ <-- int|string] <-- (DB driver) <-- (DB column) int + */ +class TimestampCast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): Time { + if (! is_int($value) && ! is_string($value)) { + self::invalidTypeValueError($value); + } + + return Time::createFromTimestamp((int) $value); + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): int { + if (! $value instanceof Time) { + self::invalidTypeValueError($value); + } + + return $value->getTimestamp(); + } +} diff --git a/system/DataCaster/Cast/URICast.php b/system/DataCaster/Cast/URICast.php new file mode 100644 index 000000000000..63f4d2271a78 --- /dev/null +++ b/system/DataCaster/Cast/URICast.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +use CodeIgniter\HTTP\URI; + +/** + * Class URICast + * + * (PHP) [URI --> string] --> (DB driver) --> (DB column) string + * [ <-- string] <-- (DB driver) <-- (DB column) string + */ +class URICast extends BaseCast +{ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): URI { + if (! is_string($value)) { + self::invalidTypeValueError($value); + } + + return new URI($value); + } + + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { + if (! $value instanceof URI) { + self::invalidTypeValueError($value); + } + + return (string) $value; + } +} diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php new file mode 100644 index 000000000000..854a0b50b32c --- /dev/null +++ b/system/DataCaster/DataCaster.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster; + +use CodeIgniter\DataCaster\Cast\ArrayCast; +use CodeIgniter\DataCaster\Cast\BooleanCast; +use CodeIgniter\DataCaster\Cast\CastInterface; +use CodeIgniter\DataCaster\Cast\CSVCast; +use CodeIgniter\DataCaster\Cast\DatetimeCast; +use CodeIgniter\DataCaster\Cast\FloatCast; +use CodeIgniter\DataCaster\Cast\IntBoolCast; +use CodeIgniter\DataCaster\Cast\IntegerCast; +use CodeIgniter\DataCaster\Cast\JsonCast; +use CodeIgniter\DataCaster\Cast\TimestampCast; +use CodeIgniter\DataCaster\Cast\URICast; +use CodeIgniter\Entity\Cast\CastInterface as EntityCastInterface; +use CodeIgniter\Entity\Exceptions\CastException; +use InvalidArgumentException; + +final class DataCaster +{ + /** + * Array of field names and the type of value to cast. + * + * @var array [field => type] + */ + private array $types = []; + + /** + * Convert handlers + * + * @var array [type => classname] + */ + private array $castHandlers = [ + 'array' => ArrayCast::class, + 'bool' => BooleanCast::class, + 'boolean' => BooleanCast::class, + 'csv' => CSVCast::class, + 'datetime' => DatetimeCast::class, + 'double' => FloatCast::class, + 'float' => FloatCast::class, + 'int' => IntegerCast::class, + 'integer' => IntegerCast::class, + 'int-bool' => IntBoolCast::class, + 'json' => JsonCast::class, + 'timestamp' => TimestampCast::class, + 'uri' => URICast::class, + ]; + + /** + * @param array|null $castHandlers Custom convert handlers + * @param array|null $types [field => type] + * @param object|null $helper Helper object. + * @param bool $strict Strict mode? Set to false for casts for Entity. + */ + public function __construct( + ?array $castHandlers = null, + ?array $types = null, + private readonly ?object $helper = null, + private readonly bool $strict = true + ) { + $this->castHandlers = array_merge($this->castHandlers, $castHandlers); + + if ($types !== null) { + $this->setTypes($types); + } + + if ($this->strict) { + foreach ($this->castHandlers as $handler) { + if ( + ! is_subclass_of($handler, CastInterface::class) + && ! is_subclass_of($handler, EntityCastInterface::class) + ) { + throw new InvalidArgumentException( + 'Invalid class type. It must implement CastInterface. class: ' . $handler + ); + } + } + } + } + + /** + * This method is only for Entity. + * + * @TODO if Entity::$casts is readonly, we don't need this method. + * + * @param array $types [field => type] + * + * @return $this + * + * @internal + */ + public function setTypes(array $types): static + { + $this->types = $types; + + return $this; + } + + /** + * Provides the ability to cast an item as a specific data type. + * Add ? at the beginning of the type (i.e. ?string) to get `null` + * instead of casting $value when $value is null. + * + * @param mixed $value The value to convert + * @param string $field The field name + * @param string $method Allowed to "get" and "set" + * @phpstan-param 'get'|'set' $method + */ + public function castAs(mixed $value, string $field, string $method = 'get'): mixed + { + // If the type is not defined, return as it is. + if (! isset($this->types[$field])) { + return $value; + } + + $type = $this->types[$field]; + + $isNullable = false; + + // Is nullable? + if (str_starts_with($type, '?')) { + $isNullable = true; + + if ($value === null) { + return null; + } + + $type = substr($type, 1); + } elseif ($value === null) { + if ($this->strict) { + $message = 'Field "' . $field . '" is not nullable, but null was passed.'; + + throw new InvalidArgumentException($message); + } + } + + // In order not to create a separate handler for the + // json-array type, we transform the required one. + $type = ($type === 'json-array') ? 'json[array]' : $type; + + $params = []; + + // Attempt to retrieve additional parameters if specified + // type[param, param2,param3] + if (preg_match('/\A(.+)\[(.+)\]\z/', $type, $matches)) { + $type = $matches[1]; + $params = array_map('trim', explode(',', $matches[2])); + } + + if ($isNullable) { + $params[] = 'nullable'; + } + + $type = trim($type, '[]'); + + $handlers = $this->castHandlers; + + if (! isset($handlers[$type])) { + throw new InvalidArgumentException( + 'No such handler for "' . $field . '". Invalid type: ' . $type + ); + } + + $handler = $handlers[$type]; + + if ( + ! $this->strict + && ! is_subclass_of($handler, CastInterface::class) + && ! is_subclass_of($handler, EntityCastInterface::class) + ) { + throw CastException::forInvalidInterface($handler); + } + + return $handler::$method($value, $params, $this->helper); + } +} diff --git a/system/DataCaster/Exceptions/CastException.php b/system/DataCaster/Exceptions/CastException.php new file mode 100644 index 000000000000..ff328b3deda1 --- /dev/null +++ b/system/DataCaster/Exceptions/CastException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Exceptions; + +use CodeIgniter\Entity\Exceptions\CastException as EntityCastException; + +/** + * CastException is thrown for invalid cast initialization and management. + */ +class CastException extends EntityCastException +{ +} diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php new file mode 100644 index 000000000000..b79ede5b08ff --- /dev/null +++ b/system/DataConverter/DataConverter.php @@ -0,0 +1,202 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataConverter; + +use Closure; +use CodeIgniter\DataCaster\DataCaster; +use CodeIgniter\Entity\Entity; + +/** + * PHP data <==> DataSource data converter + * + * @see \CodeIgniter\DataConverter\DataConverterTest + * + * @template TEntity of object + */ +final class DataConverter +{ + /** + * The data caster. + */ + private readonly DataCaster $dataCaster; + + /** + * @param array $castHandlers Custom convert handlers + * + * @internal + */ + public function __construct( + /** + * Type definitions. + * + * @var array [column => type] + */ + private readonly array $types, + array $castHandlers = [], + /** + * Helper object. + */ + private readonly ?object $helper = null, + /** + * Static reconstruct method name or closure to reconstruct an object. + * Used by reconstruct(). + * + * @phpstan-var (Closure(array): TEntity)|string|null + */ + private readonly Closure|string|null $reconstructor = 'reconstruct', + /** + * Extract method name or closure to extract data from an object. + * Used by extract(). + * + * @phpstan-var (Closure(TEntity, bool, bool): array)|string|null + */ + private readonly Closure|string|null $extractor = null, + ) { + $this->dataCaster = new DataCaster($castHandlers, $types, $this->helper); + } + + /** + * Converts data from DataSource to PHP array with specified type values. + * + * @param array $data DataSource data + * + * @internal + */ + public function fromDataSource(array $data): array + { + foreach (array_keys($this->types) as $field) { + if (array_key_exists($field, $data)) { + $data[$field] = $this->dataCaster->castAs($data[$field], $field, 'get'); + } + } + + return $data; + } + + /** + * Converts PHP array to data for DataSource field types. + * + * @param array $phpData PHP data + * + * @internal + */ + public function toDataSource(array $phpData): array + { + foreach (array_keys($this->types) as $field) { + if (array_key_exists($field, $phpData)) { + $phpData[$field] = $this->dataCaster->castAs($phpData[$field], $field, 'set'); + } + } + + return $phpData; + } + + /** + * Takes database data array and creates a specified type object. + * + * @param class-string $classname + * @phpstan-param class-string $classname + * @param array $row Raw data from database + * + * @phpstan-return TEntity + * + * @internal + */ + public function reconstruct(string $classname, array $row): object + { + $phpData = $this->fromDataSource($row); + + // Use static reconstruct method. + if (is_string($this->reconstructor) && method_exists($classname, $this->reconstructor)) { + $method = $this->reconstructor; + + return $classname::$method($phpData); + } + + // Use closure to reconstruct. + if ($this->reconstructor instanceof Closure) { + $closure = $this->reconstructor; + + return $closure($phpData); + } + + $classObj = new $classname(); + + if ($classObj instanceof Entity) { + $classObj->injectRawData($phpData); + $classObj->syncOriginal(); + + return $classObj; + } + + $classSet = Closure::bind(function ($key, $value) { + $this->{$key} = $value; + }, $classObj, $classname); + + foreach ($phpData as $key => $value) { + $classSet($key, $value); + } + + return $classObj; + } + + /** + * Takes an object and extract properties as an array. + * + * @param bool $onlyChanged Only for CodeIgniter's Entity. If true, only returns + * values that have changed since object creation. + * @param bool $recursive Only for CodeIgniter's Entity. If true, inner + * entities will be cast as array as well. + * + * @return array + * + * @internal + */ + public function extract(object $object, bool $onlyChanged = false, bool $recursive = false): array + { + // Use extractor method. + if (is_string($this->extractor) && method_exists($object, $this->extractor)) { + $method = $this->extractor; + $row = $object->{$method}($onlyChanged, $recursive); + + return $this->toDataSource($row); + } + + // Use closure to extract. + if ($this->extractor instanceof Closure) { + $closure = $this->extractor; + $row = $closure($object, $onlyChanged, $recursive); + + return $this->toDataSource($row); + } + + if ($object instanceof Entity) { + $row = $object->toRawArray($onlyChanged, $recursive); + + return $this->toDataSource($row); + } + + $array = (array) $object; + + $row = []; + + foreach ($array as $key => $value) { + $key = preg_replace('/\000.*\000/', '', $key); + + $row[$key] = $value; + } + + return $this->toDataSource($row); + } +} diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 65dea86da2b3..1f0807cdbbde 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1,5 +1,7 @@ db = $db; // If it contains `,`, it has multiple tables - if (is_string($tableName) && strpos($tableName, ',') === false) { + if (is_string($tableName) && ! str_contains($tableName, ',')) { $this->tableName = $tableName; // @TODO remove alias if exists } else { $this->tableName = ''; @@ -511,7 +514,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string throw DataException::forEmptyInputGiven('Select'); } - if (strpos($select, ',') !== false) { + if (str_contains($select, ',')) { throw DataException::forInvalidArgument('column name not separated by comma'); } @@ -538,7 +541,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string */ protected function createAliasFromTable(string $item): string { - if (strpos($item, '.') !== false) { + if (str_contains($item, '.')) { $item = explode('.', $item); return end($item); @@ -574,7 +577,7 @@ public function from($from, bool $overwrite = false): self } foreach ((array) $from as $table) { - if (strpos($table, ',') !== false) { + if (str_contains($table, ',')) { $this->from(explode(',', $table)); } else { $table = trim($table); @@ -763,7 +766,7 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type $op = trim(current($op)); // Does the key end with operator? - if (substr($k, -strlen($op)) === $op) { + if (str_ends_with($k, $op)) { $k = rtrim(substr($k, 0, -strlen($op))); $op = " {$op}"; } else { @@ -1490,6 +1493,11 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = */ public function limit(?int $value = null, ?int $offset = 0) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll && $value === 0) { + $value = null; + } + if ($value !== null) { $this->QBLimit = $value; } @@ -1607,6 +1615,11 @@ protected function compileFinalQuery(string $sql): string */ public function get(?int $limit = null, int $offset = 0, bool $reset = true) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll && $limit === 0) { + $limit = null; + } + if ($limit !== null) { $this->limit($limit, $offset); } @@ -1740,7 +1753,12 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo $this->where($where); } - if ($limit !== null && $limit !== 0) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll && $limit === 0) { + $limit = null; + } + + if ($limit !== null) { $this->limit($limit, $offset); } @@ -2324,7 +2342,7 @@ public function insert($set = null, ?bool $escape = null) */ protected function removeAlias(string $from): string { - if (strpos($from, ' ') !== false) { + if (str_contains($from, ' ')) { // if the alias is written with the AS keyword, remove it $from = preg_replace('/\s+AS\s+/i', ' ', $from); @@ -2458,7 +2476,12 @@ public function update($set = null, $where = null, ?int $limit = null): bool $this->where($where); } - if ($limit !== null && $limit !== 0) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll && $limit === 0) { + $limit = null; + } + + if ($limit !== null) { if (! $this->canLimitWhereUpdates) { throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.'); } @@ -2499,10 +2522,18 @@ protected function _update(string $table, array $values): string $valStr[] = $key . ' = ' . $val; } + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll) { + return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr) + . $this->compileWhereHaving('QBWhere') + . $this->compileOrderBy() + . ($this->QBLimit ? $this->_limit(' ', true) : ''); + } + return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr) . $this->compileWhereHaving('QBWhere') . $this->compileOrderBy() - . ($this->QBLimit ? $this->_limit(' ', true) : ''); + . ($this->QBLimit !== false ? $this->_limit(' ', true) : ''); } /** @@ -2768,7 +2799,12 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true) $sql = $this->_delete($this->removeAlias($table)); - if ($limit !== null && $limit !== 0) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll && $limit === 0) { + $limit = null; + } + + if ($limit !== null) { $this->QBLimit = $limit; } @@ -2972,12 +3008,12 @@ protected function trackAliases($table) // Does the string contain a comma? If so, we need to separate // the string into discreet statements - if (strpos($table, ',') !== false) { + if (str_contains($table, ',')) { return $this->trackAliases(explode(',', $table)); } // if a table alias is used we can recognize it by a space - if (strpos($table, ' ') !== false) { + if (str_contains($table, ' ')) { // if the alias is written with the AS keyword, remove it $table = preg_replace('/\s+AS\s+/i', ' ', $table); @@ -3034,7 +3070,12 @@ protected function compileSelect($selectOverride = false): string . $this->compileWhereHaving('QBHaving') . $this->compileOrderBy(); - if ($this->QBLimit) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll) { + if ($this->QBLimit) { + $sql = $this->_limit($sql . "\n"); + } + } elseif ($this->QBLimit !== false || $this->QBOffset) { $sql = $this->_limit($sql . "\n"); } @@ -3117,11 +3158,11 @@ protected function compileWhereHaving(string $qbKey): string if (! empty($matches[4])) { $protectIdentifiers = false; - if (strpos($matches[4], '.') !== false) { + if (str_contains($matches[4], '.')) { $protectIdentifiers = true; } - if (strpos($matches[4], ':') === false) { + if (! str_contains($matches[4], ':')) { $matches[4] = $this->db->protectIdentifiers(trim($matches[4]), false, $protectIdentifiers); } diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 93bde5ddca6d..e0bd7bb51217 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1,5 +1,7 @@ + */ + protected array $dateFormat = [ + 'date' => 'Y-m-d', + 'datetime' => 'Y-m-d H:i:s', + 'datetime-ms' => 'Y-m-d H:i:s.v', + 'datetime-us' => 'Y-m-d H:i:s.u', + 'time' => 'H:i:s', + ]; + /** * Saves our connection settings. */ public function __construct(array $params) { + if (isset($params['dateFormat'])) { + $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']); + unset($params['dateFormat']); + } + foreach ($params as $key => $value) { if (property_exists($this, $key)) { $this->{$key} = $value; @@ -1001,10 +1029,10 @@ 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|string $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 $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 array|string * @phpstan-return ($item is array ? array : string) @@ -1025,6 +1053,9 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro return $escapedArray; } + // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int. + $item = (string) $item; + // This is basically a bug fix for queries that use MAX, MIN, etc. // If a parenthesis is found we know that we do not need to // escape the data or add a prefix. There's probably a more graceful @@ -1062,7 +1093,7 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro // Break the string apart if it contains periods, then insert the table prefix // in the correct location, assuming the period doesn't indicate that we're dealing // with an alias. While we're at it, we will escape the components - if (strpos($item, '.') !== false) { + if (str_contains($item, '.')) { return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists); } @@ -1074,11 +1105,11 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro // Is there a table prefix? If not, no need to insert it if ($this->DBPrefix !== '') { // Verify table prefix and replace if necessary - if ($this->swapPre !== '' && strpos($item, $this->swapPre) === 0) { + if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) { $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item); } // Do we prefix an item with no segments? - elseif ($prefixSingle === true && strpos($item, $this->DBPrefix) !== 0) { + elseif ($prefixSingle === true && ! str_starts_with($item, $this->DBPrefix)) { $item = $this->DBPrefix . $item; } } @@ -1140,11 +1171,11 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti } // Verify table prefix and replace if necessary - if ($this->swapPre !== '' && strpos($parts[$i], $this->swapPre) === 0) { + if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) { $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]); } // We only add the table prefix if it does not already exist - elseif (strpos($parts[$i], $this->DBPrefix) !== 0) { + elseif (! str_starts_with($parts[$i], $this->DBPrefix)) { $parts[$i] = $this->DBPrefix . $parts[$i]; } @@ -1159,6 +1190,24 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti return $item . $alias; } + /** + * Escape the SQL Identifier + * + * This function escapes single identifier. + * + * @param non-empty-string $item + */ + public function escapeIdentifier(string $item): string + { + return $this->escapeChar + . str_replace( + $this->escapeChar, + $this->escapeChar . $this->escapeChar, + $item + ) + . $this->escapeChar; + } + /** * Escape the SQL Identifiers * @@ -1187,7 +1236,7 @@ public function escapeIdentifiers($item) if (ctype_digit($item) || $item[0] === "'" || ($this->escapeChar !== '"' && $item[0] === '"') - || strpos($item, '(') !== false) { + || str_contains($item, '(')) { return $item; } @@ -1207,7 +1256,7 @@ public function escapeIdentifiers($item) foreach ($this->reservedIdentifiers as $id) { /** @psalm-suppress NoValue I don't know why ERROR. */ - if (strpos($item, '.' . $id) !== false) { + if (str_contains($item, '.' . $id)) { return preg_replace( '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i', $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.', @@ -1257,7 +1306,7 @@ abstract public function affectedRows(): int; public function escape($str) { if (is_array($str)) { - return array_map([&$this, 'escape'], $str); + return array_map($this->escape(...), $str); } /** @psalm-suppress NoValue I don't know why ERROR. */ @@ -1353,7 +1402,7 @@ public function callFunction(string $functionName, ...$params): bool { $driver = $this->getDriverFunctionPrefix(); - if (strpos($driver, $functionName) === false) { + if (! str_contains($driver, $functionName)) { $functionName = $driver . $functionName; } diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index ea16de594c2b..8c4f252cfb7e 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -1,5 +1,7 @@ db->isWriteType($query)) { + if ($this->db->isWriteType((string) $query)) { return true; } diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 8720b5fe111c..efbc16722148 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -1,5 +1,7 @@ ' . $newline; foreach ($row as $key => $val) { - $val = (! empty($val)) ? xml_convert($val) : ''; + $val = (! empty($val)) ? xml_convert((string) $val) : ''; $xml .= $tab . $tab . '<' . $key . '>' . $val . '' . $newline; } diff --git a/system/Database/Config.php b/system/Database/Config.php index 92556c6d8fef..03a0dd15743b 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -1,5 +1,7 @@ {$group})) { - throw new InvalidArgumentException($group . ' is not a valid database connection group.'); + throw new InvalidArgumentException('"' . $group . '" is not a valid database connection group.'); } $config = $dbConfig->{$group}; diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index ed45933734a2..2f1c932c461d 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -1,5 +1,7 @@ parseDSN($params); } @@ -130,7 +132,7 @@ protected function parseDSN(array $params): array */ protected function initDriver(string $driver, string $class, $argument): object { - $classname = (strpos($driver, '\\') === false) + $classname = (! str_contains($driver, '\\')) ? "CodeIgniter\\Database\\{$driver}\\{$class}" : $driver . '\\' . $class; diff --git a/system/Database/Exceptions/DataException.php b/system/Database/Exceptions/DataException.php index 09b9ff329708..18a54171cc29 100644 --- a/system/Database/Exceptions/DataException.php +++ b/system/Database/Exceptions/DataException.php @@ -1,5 +1,7 @@ db->query(sprintf($ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr, $dbName, $this->db->charset, $this->db->DBCollat))) { + if (! $this->db->query( + sprintf( + $ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr, + $this->db->escapeIdentifier($dbName), + $this->db->charset, + $this->db->DBCollat + ) + )) { // @codeCoverageIgnoreStart if ($this->db->DBDebug) { throw new DatabaseException('Unable to create the specified database.'); @@ -284,7 +293,9 @@ public function dropDatabase(string $dbName): bool return false; } - if (! $this->db->query(sprintf($this->dropDatabaseStr, $dbName))) { + if (! $this->db->query( + sprintf($this->dropDatabaseStr, $this->db->escapeIdentifier($dbName)) + )) { if ($this->db->DBDebug) { throw new DatabaseException('Unable to drop the specified database.'); } @@ -293,7 +304,11 @@ public function dropDatabase(string $dbName): bool } if (! empty($this->db->dataCache['db_names'])) { - $key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true); + $key = array_search( + strtolower($dbName), + array_map('strtolower', $this->db->dataCache['db_names']), + true + ); if ($key !== false) { unset($this->db->dataCache['db_names'][$key]); } @@ -368,7 +383,7 @@ public function addField($fields) ]); $this->addKey('id', true); } else { - if (strpos($fields, ' ') === false) { + if (! str_contains($fields, ' ')) { throw new InvalidArgumentException('Field information is required for that operation.'); } @@ -635,7 +650,7 @@ public function dropTable(string $tableName, bool $ifExists = false, bool $casca return false; } - if ($this->db->DBPrefix && strpos($tableName, $this->db->DBPrefix) === 0) { + if ($this->db->DBPrefix && str_starts_with($tableName, $this->db->DBPrefix)) { $tableName = substr($tableName, strlen($this->db->DBPrefix)); } diff --git a/system/Database/Migration.php b/system/Database/Migration.php index f12509d6264a..4386b7448e80 100644 --- a/system/Database/Migration.php +++ b/system/Database/Migration.php @@ -1,5 +1,7 @@ get() ->getResultObject(); - return count($migration) ? $migration[0]->version : 0; + return $migration === [] ? '0' : $migration[0]->version; } /** diff --git a/system/Database/ModelFactory.php b/system/Database/ModelFactory.php deleted file mode 100644 index 76a30e6c1d30..000000000000 --- a/system/Database/ModelFactory.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Database; - -use CodeIgniter\Config\Factories; - -/** - * Returns new or shared Model instances - * - * @deprecated Use CodeIgniter\Config\Factories::models() - * - * @codeCoverageIgnore - * @see \CodeIgniter\Database\ModelFactoryTest - */ -class ModelFactory -{ - /** - * Creates new Model instances or returns a shared instance - * - * @return mixed - */ - public static function get(string $name, bool $getShared = true, ?ConnectionInterface $connection = null) - { - return Factories::models($name, ['getShared' => $getShared], $connection); - } - - /** - * Helper method for injecting mock instances while testing. - * - * @param object $instance - */ - public static function injectMock(string $name, $instance) - { - Factories::injectMock('models', $name, $instance); - } - - /** - * Resets the static arrays - */ - public static function reset() - { - Factories::reset('models'); - } -} diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index 096bbedd888c..81434bc3645d 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -1,5 +1,7 @@ escapeIdentifiers($this->database); + $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database); if ($tableName !== null) { return $sql . ' LIKE ' . $this->escape($tableName); diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index adc6c6cc7267..132bde1258cc 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -1,5 +1,7 @@ db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) { + if ($this->db->charset !== '' && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) { $sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset); } - if (! empty($this->db->DBCollat) && ! strpos($sql, 'COLLATE')) { + if ($this->db->DBCollat !== '' && ! strpos($sql, 'COLLATE')) { $sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat); } diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 594fcba24901..e9a6c6d10e30 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -1,5 +1,7 @@ username; @@ -287,7 +289,7 @@ protected function _listColumns(string $table = ''): string */ protected function _fieldData(string $table): array { - if (strpos($table, '.') !== false) { + if (str_contains($table, '.')) { sscanf($table, '%[^.].%s', $owner, $table); } else { $owner = $this->username; @@ -331,7 +333,7 @@ protected function _fieldData(string $table): array */ protected function _indexData(string $table): array { - if (strpos($table, '.') !== false) { + if (str_contains($table, '.')) { sscanf($table, '%[^.].%s', $owner, $table); } else { $owner = $this->username; @@ -629,7 +631,7 @@ protected function buildDSN() return; } - $isEasyConnectableHostName = $this->hostname !== '' && strpos($this->hostname, '/') === false && strpos($this->hostname, ':') === false; + $isEasyConnectableHostName = $this->hostname !== '' && ! str_contains($this->hostname, '/') && ! str_contains($this->hostname, ':'); $easyConnectablePort = ! empty($this->port) && ctype_digit($this->port) ? ':' . $this->port : ''; $easyConnectableDatabase = $this->database !== '' ? '/' . ltrim($this->database, '/') : ''; diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 4de36ac91e6d..69a42ed1dbeb 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -1,5 +1,7 @@ db->escapeIdentifiers($processedField['name']) . ' IN ' . $processedField['length'] . ')'; diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index 93d260707fb0..c267f8061377 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -1,5 +1,7 @@ $value) { - if (strtoupper($value[$key]) === 'NULL') { + if (strtoupper((string) $value[$key]) === 'NULL') { $values[$arrayKey][$key] = 'DEFAULT'; } } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 7da3d26c9dd8..45e30eb65365 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -1,5 +1,7 @@ indexdef))); $obj->fields = array_map(static fn ($v) => trim($v), $_fields); - if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) { + if (str_starts_with($row->indexdef, 'CREATE UNIQUE INDEX pk')) { $obj->type = 'PRIMARY'; } else { - $obj->type = (strpos($row->indexdef, 'CREATE UNIQUE') === 0) ? 'UNIQUE' : 'INDEX'; + $obj->type = (str_starts_with($row->indexdef, 'CREATE UNIQUE')) ? 'UNIQUE' : 'INDEX'; } $retVal[$obj->name] = $obj; @@ -496,7 +498,7 @@ protected function buildDSN() } // If UNIX sockets are used, we shouldn't set a port - if (strpos($this->hostname, '/') !== false) { + if (str_contains($this->hostname, '/')) { $this->port = ''; } diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index 8af1507c6b2a..7a61bed5a57c 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -1,5 +1,7 @@ QBFrom as $value) { - $from[] = strpos($value, '(SELECT') === 0 ? $value : $this->getFullName($value); + $from[] = str_starts_with($value, '(SELECT') ? $value : $this->getFullName($value); } return implode(', ', $from); @@ -280,7 +283,7 @@ private function getFullName(string $table): string { $alias = ''; - if (strpos($table, ' ') !== false) { + if (str_contains($table, ' ')) { $alias = explode(' ', $table); $table = array_shift($alias); $alias = ' ' . implode(' ', $alias); @@ -306,6 +309,15 @@ private function addIdentity(string $fullTable, string $insert): string */ protected function _limit(string $sql, bool $offsetIgnore = false): string { + // SQL Server cannot handle `LIMIT 0`. + // DatabaseException: + // [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]The number of + // rows provided for a FETCH clause must be greater then zero. + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if (! $limitZeroAsAll && $this->QBLimit === 0) { + return "SELECT * \nFROM " . $this->_fromTables() . ' WHERE 1=0 '; + } + if (empty($this->QBOrderBy)) { $sql .= ' ORDER BY (SELECT NULL) '; } @@ -439,7 +451,7 @@ protected function maxMinAvgSum(string $select = '', string $alias = '', string throw DataException::forEmptyInputGiven('Select'); } - if (strpos($select, ',') !== false) { + if (str_contains($select, ',')) { throw DataException::forInvalidArgument('Column name not separated by comma'); } @@ -588,7 +600,12 @@ protected function compileSelect($selectOverride = false): string . $this->compileOrderBy(); // ORDER BY // LIMIT - if ($this->QBLimit) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll) { + if ($this->QBLimit) { + $sql = $this->_limit($sql . "\n"); + } + } elseif ($this->QBLimit !== false || $this->QBOffset) { $sql = $this->_limit($sql . "\n"); } @@ -603,6 +620,11 @@ protected function compileSelect($selectOverride = false): string */ public function get(?int $limit = null, int $offset = 0, bool $reset = true) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll && $limit === 0) { + $limit = null; + } + if ($limit !== null) { $this->limit($limit, $offset); } diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 77a1ab34bcf3..411470d8e34c 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -1,5 +1,7 @@ hostname, ',') === false && $this->port !== '') { + if (! str_contains($this->hostname, ',') && $this->port !== '') { $this->hostname .= ', ' . $this->port; } @@ -190,7 +192,7 @@ protected function _escapeString(string $str): string */ public function insertID(): int { - return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0; + return (int) ($this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0); } /** @@ -253,10 +255,10 @@ protected function _indexData(string $table): array $_fields = explode(',', trim($row->index_keys)); $obj->fields = array_map(static fn ($v) => trim($v), $_fields); - if (strpos($row->index_description, 'primary key located on') !== false) { + if (str_contains($row->index_description, 'primary key located on')) { $obj->type = 'PRIMARY'; } else { - $obj->type = (strpos($row->index_description, 'nonclustered, unique') !== false) ? 'UNIQUE' : 'INDEX'; + $obj->type = (str_contains($row->index_description, 'nonclustered, unique')) ? 'UNIQUE' : 'INDEX'; } $retVal[$obj->name] = $obj; diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 5dead5b3a308..cae4eda2a4f5 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -1,5 +1,7 @@ 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) { + $sql = sprintf( + $this->createDatabaseIfStr, + $dbName, + $this->db->escapeIdentifier($dbName) + ); + } else { + $sql = sprintf( + $this->createDatabaseStr, + $this->db->escapeIdentifier($dbName) + ); + } + + try { + if (! $this->db->query($sql)) { + // @codeCoverageIgnoreStart + if ($this->db->DBDebug) { + throw new DatabaseException('Unable to create the specified database.'); + } + + return false; + // @codeCoverageIgnoreEnd + } + + if (isset($this->db->dataCache['db_names'])) { + $this->db->dataCache['db_names'][] = $dbName; + } + + return true; + } catch (Throwable $e) { + if ($this->db->DBDebug) { + throw new DatabaseException('Unable to create the specified database.', 0, $e); + } + + return false; // @codeCoverageIgnore + } + } + /** * CREATE TABLE attributes */ @@ -324,8 +375,18 @@ protected function _attributeType(array &$attributes) break; case 'ENUM': - $attributes['TYPE'] = 'TEXT'; - $attributes['CONSTRAINT'] = null; + // in char(n) and varchar(n), the n defines the string length in + // bytes (0 to 8,000). + // https://learn.microsoft.com/en-us/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-ver16#remarks + $maxLength = max( + array_map( + static fn ($value) => strlen($value), + $attributes['CONSTRAINT'] + ) + ); + + $attributes['TYPE'] = 'VARCHAR'; + $attributes['CONSTRAINT'] = $maxLength; break; case 'TIMESTAMP': diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index f752e769349b..f3a4a14c2bc6 100755 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -1,5 +1,7 @@ database !== ':memory:' && strpos($this->database, DIRECTORY_SEPARATOR) === false) { + if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) { $this->database = WRITEPATH . $this->database; } - return (! $this->password) + $sqlite = (! $this->password) ? new SQLite3($this->database) : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password); + + $sqlite->enableExceptions(true); + + return $sqlite; } catch (Exception $e) { throw new DatabaseException('SQLite3 error: ' . $e->getMessage()); } @@ -144,7 +149,7 @@ protected function execute(string $sql) return $this->isWriteType($sql) ? $this->connID->exec($sql) : $this->connID->query($sql); - } catch (ErrorException $e) { + } catch (Exception $e) { log_message('error', (string) $e); if ($this->DBDebug) { diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 4be650e03155..32e447829c29 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -1,5 +1,7 @@ db->escapeIdentifiers($processedField['name']) . ' IN ' . $processedField['length'] . ')'; } diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index e892e73bde10..21dc4c2fdeff 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -1,5 +1,7 @@ statement->bindValue($key + 1, $item, $bindType); } - $this->result = $this->statement->execute(); + try { + $this->result = $this->statement->execute(); + } catch (Exception $e) { + if ($this->db->DBDebug) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + return false; + } return $this->result !== false; } diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php index f3887b044b49..70d8dc47d2b6 100644 --- a/system/Database/SQLite3/Result.php +++ b/system/Database/SQLite3/Result.php @@ -1,5 +1,7 @@ db->DBPrefix; - if (! empty($prefix) && strpos($table, $prefix) === 0) { + if (! empty($prefix) && str_starts_with($table, $prefix)) { $table = substr($table, strlen($prefix)); } @@ -430,7 +432,7 @@ protected function formatFields($fields) */ private function isIntegerType(string $type): bool { - return strpos(strtoupper($type), 'INT') !== false; + return str_contains(strtoupper($type), 'INT'); } /** diff --git a/system/Database/SQLite3/Utils.php b/system/Database/SQLite3/Utils.php index b8f45dd54196..93f7f0e00db7 100644 --- a/system/Database/SQLite3/Utils.php +++ b/system/Database/SQLite3/Utils.php @@ -1,5 +1,7 @@ seedPath . str_replace('.php', '', $class) . '.php'; if (! is_file($path)) { diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index 6a5b5e47e5c7..dbccdcd07790 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -1,5 +1,7 @@ get_class($exception), - 'type' => get_class($exception), + 'title' => $exception::class, + 'type' => $exception::class, 'code' => $statusCode, 'message' => $exception->getMessage(), 'file' => $exception->getFile(), @@ -114,7 +116,7 @@ private function maskData($args, array $keysToMask, string $path = '') $explode = explode('/', $keyToMask); $index = end($explode); - if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) { + if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) { if (is_array($args) && array_key_exists($index, $args)) { $args[$index] = '******************'; } elseif ( @@ -176,7 +178,7 @@ protected static function highlightFile(string $file, int $lineNumber, int $line try { $source = file_get_contents($file); - } catch (Throwable $e) { + } catch (Throwable) { return false; } diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php index 09c6de0d5b7e..d6f97b76a32b 100644 --- a/system/Debug/ExceptionHandler.php +++ b/system/Debug/ExceptionHandler.php @@ -1,5 +1,7 @@ setStatusCode($statusCode); - } catch (HTTPException $e) { + } catch (HTTPException) { // Workaround for invalid HTTP status code. $statusCode = 500; $response->setStatusCode($statusCode); @@ -73,7 +75,7 @@ public function handle( ); } - if (strpos($request->getHeaderLine('accept'), 'text/html') === false) { + if (! str_contains($request->getHeaderLine('accept'), 'text/html')) { $data = (ENVIRONMENT === 'development' || ENVIRONMENT === 'testing') ? $this->collectVars($exception, $statusCode) : ''; diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php index bbfcb6ba70ab..d0379e2abd63 100644 --- a/system/Debug/ExceptionHandlerInterface.php +++ b/system/Debug/ExceptionHandlerInterface.php @@ -1,5 +1,7 @@ exceptionHandler(...)); + set_error_handler($this->errorHandler(...)); register_shutdown_function([$this, 'shutdownHandler']); } @@ -124,12 +126,18 @@ public function exceptionHandler(Throwable $exception) [$statusCode, $exitCode] = $this->determineCodes($exception); + $this->request = Services::request(); + if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { - log_message('critical', get_class($exception) . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [ - 'message' => $exception->getMessage(), - 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file - 'exLine' => $exception->getLine(), // {line} refers to THIS line - 'trace' => self::renderBacktrace($exception->getTrace()), + $uri = $this->request->getPath() === '' ? '/' : $this->request->getPath(); + $routeInfo = '[Method: ' . $this->request->getMethod() . ', Route: ' . $uri . ']'; + + log_message('critical', $exception::class . ": {message}\n{routeInfo}\nin {exFile} on line {exLine}.\n{trace}", [ + 'message' => $exception->getMessage(), + 'routeInfo' => $routeInfo, + 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file + 'exLine' => $exception->getLine(), // {line} refers to THIS line + 'trace' => self::renderBacktrace($exception->getTrace()), ]); // Get the first exception. @@ -138,7 +146,7 @@ public function exceptionHandler(Throwable $exception) while ($prevException = $last->getPrevious()) { $last = $prevException; - log_message('critical', '[Caused by] ' . get_class($prevException) . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [ + log_message('critical', '[Caused by] ' . $prevException::class . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [ 'message' => $prevException->getMessage(), 'exFile' => clean_path($prevException->getFile()), // {file} refers to THIS file 'exLine' => $prevException->getLine(), // {line} refers to THIS line @@ -147,7 +155,6 @@ public function exceptionHandler(Throwable $exception) } } - $this->request = Services::request(); $this->response = Services::response(); if (method_exists($this->config, 'handler')) { @@ -168,7 +175,7 @@ public function exceptionHandler(Throwable $exception) if (! is_cli()) { try { $this->response->setStatusCode($statusCode); - } catch (HTTPException $e) { + } catch (HTTPException) { // Workaround for invalid HTTP status code. $statusCode = 500; $this->response->setStatusCode($statusCode); @@ -178,7 +185,7 @@ public function exceptionHandler(Throwable $exception) header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode); } - if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) { + if (! str_contains($this->request->getHeaderLine('accept'), 'text/html')) { $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send(); exit($exitCode); @@ -236,7 +243,7 @@ public function shutdownHandler() if ($this->exceptionCaughtByExceptionHandler instanceof Throwable) { $message .= "\n【Previous Exception】\n" - . get_class($this->exceptionCaughtByExceptionHandler) . "\n" + . $this->exceptionCaughtByExceptionHandler::class . "\n" . $this->exceptionCaughtByExceptionHandler->getMessage() . "\n" . $this->exceptionCaughtByExceptionHandler->getTraceAsString(); } @@ -348,8 +355,8 @@ protected function collectVars(Throwable $exception, int $statusCode): array } return [ - 'title' => get_class($exception), - 'type' => get_class($exception), + 'title' => $exception::class, + 'type' => $exception::class, 'code' => $statusCode, 'message' => $exception->getMessage(), 'file' => $exception->getFile(), @@ -389,7 +396,7 @@ private function maskData($args, array $keysToMask, string $path = '') $explode = explode('/', $keyToMask); $index = end($explode); - if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) { + if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) { if (is_array($args) && array_key_exists($index, $args)) { $args[$index] = '******************'; } elseif ( @@ -473,25 +480,13 @@ private function handleDeprecationError(string $message, ?string $file = null, ? */ public static function cleanPath(string $file): string { - switch (true) { - case strpos($file, APPPATH) === 0: - $file = 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH)); - break; - - case strpos($file, SYSTEMPATH) === 0: - $file = 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH)); - break; - - case strpos($file, FCPATH) === 0: - $file = 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH)); - break; - - case defined('VENDORPATH') && strpos($file, VENDORPATH) === 0: - $file = 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH)); - break; - } - - return $file; + return match (true) { + str_starts_with($file, APPPATH) => 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH)), + str_starts_with($file, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH)), + str_starts_with($file, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH)), + defined('VENDORPATH') && str_starts_with($file, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH)), + default => $file, + }; } /** @@ -537,7 +532,7 @@ public static function highlightFile(string $file, int $lineNumber, int $lines = try { $source = file_get_contents($file); - } catch (Throwable $e) { + } catch (Throwable) { return false; } @@ -602,23 +597,12 @@ private static function renderBacktrace(array $backtrace): string $idx = $index; $idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT); - $args = implode(', ', array_map(static function ($value): string { - switch (true) { - case is_object($value): - return sprintf('Object(%s)', get_class($value)); - - case is_array($value): - return $value !== [] ? '[...]' : '[]'; - - case $value === null: - return 'null'; - - case is_resource($value): - return sprintf('resource (%s)', get_resource_type($value)); - - default: - return var_export($value, true); - } + $args = implode(', ', array_map(static fn ($value): string => match (true) { + is_object($value) => sprintf('Object(%s)', $value::class), + is_array($value) => $value !== [] ? '[...]' : '[]', + $value === null => 'null', + is_resource($value) => sprintf('resource (%s)', get_resource_type($value)), + default => var_export($value, true), }, $frame['args'])); $backtraces[] = sprintf( diff --git a/system/Debug/Iterator.php b/system/Debug/Iterator.php index 4bfb3a5bc88c..55ee0f3bce8b 100644 --- a/system/Debug/Iterator.php +++ b/system/Debug/Iterator.php @@ -1,5 +1,7 @@ getMethod()); + $data['method'] = $request->getMethod(); $data['isAJAX'] = $request->isAJAX(); $data['startTime'] = $startTime; $data['totalTime'] = $totalTime * 1000; @@ -140,8 +143,17 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques $data['vars']['post'][esc($name)] = is_array($value) ? '
' . esc(print_r($value, true)) . '
' : esc($value); } - foreach ($request->headers() as $header) { - $data['vars']['headers'][esc($header->getName())] = esc($header->getValueLine()); + foreach ($request->headers() as $name => $value) { + if ($value instanceof Header) { + $data['vars']['headers'][esc($name)] = esc($value->getValueLine()); + } else { + foreach ($value as $i => $header) { + $index = $i + 1; + $data['vars']['headers'][esc($name)] ??= ''; + $data['vars']['headers'][esc($name)] .= ' (' . $index . ') ' + . esc($header->getValueLine()); + } + } } foreach ($request->getCookie() as $name => $value) { @@ -157,8 +169,17 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques 'headers' => [], ]; - foreach ($response->headers() as $header) { - $data['vars']['response']['headers'][esc($header->getName())] = esc($header->getValueLine()); + foreach ($response->headers() as $name => $value) { + if ($value instanceof Header) { + $data['vars']['response']['headers'][esc($name)] = esc($value->getValueLine()); + } else { + foreach ($value as $i => $header) { + $index = $i + 1; + $data['vars']['response']['headers'][esc($name)] ??= ''; + $data['vars']['response']['headers'][esc($name)] .= ' (' . $index . ') ' + . esc($header->getValueLine()); + } + } } $data['config'] = Config::display(); @@ -354,10 +375,10 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r * @var IncomingRequest|null $request */ if (CI_DEBUG && ! is_cli()) { - $app = Services::codeigniter(); + $app = service('codeigniter'); - $request ??= Services::request(); - $response ??= Services::response(); + $request ??= service('request'); + $response ??= service('response'); // Disable the toolbar for downloads if ($response instanceof DownloadResponse) { @@ -389,7 +410,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r // Non-HTML formats should not include the debugbar // then we send headers saying where to find the debug data // for this response - if ($request->isAJAX() || strpos($format, 'html') === false) { + if ($request->isAJAX() || ! str_contains($format, 'html')) { $response->setHeader('Debugbar-Time', "{$time}") ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}")); @@ -412,7 +433,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r . $kintScript . PHP_EOL; - if (strpos($response->getBody(), '') !== false) { + if (str_contains($response->getBody(), '')) { $response->setBody( preg_replace( '//', @@ -442,7 +463,7 @@ public function respond() return; } - $request = Services::request(); + $request = service('request'); // If the request contains '?debugbar then we're // simply returning the loading script @@ -491,7 +512,7 @@ protected function format(string $data, string $format = 'html'): string { $data = json_decode($data, true); - if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) Services::request()->getGet('debugbar_time'), $debugbarTime)) { + if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) service('request')->getGet('debugbar_time'), $debugbarTime)) { $history = new History(); $history->setFiles( $debugbarTime[0], diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php index 4704fcdcaaeb..81cc631f98e7 100644 --- a/system/Debug/Toolbar/Collectors/BaseCollector.php +++ b/system/Debug/Toolbar/Collectors/BaseCollector.php @@ -1,5 +1,7 @@ ENVIRONMENT, 'baseURL' => $config->baseURL, 'timezone' => app_timezone(), - 'locale' => Services::request()->getLocale(), + 'locale' => service('request')->getLocale(), 'cspEnabled' => $config->CSPEnabled, ]; } diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index 5b5614ad3b9c..ee9f829b05b8 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -1,5 +1,7 @@ $path, 'name' => basename($file), diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php index fcf06e16f54a..4e5276b55b4e 100644 --- a/system/Debug/Toolbar/Collectors/History.php +++ b/system/Debug/Toolbar/Collectors/History.php @@ -1,5 +1,7 @@ controllerName(), $router->methodName()); - } catch (ReflectionException $e) { - // If we're here, the method doesn't exist - // and is likely calculated in _remap. - $method = new ReflectionMethod($router->controllerName(), '_remap'); + } catch (ReflectionException) { + try { + // If we're here, the method doesn't exist + // and is likely calculated in _remap. + $method = new ReflectionMethod($router->controllerName(), '_remap'); + } catch (ReflectionException) { + // If we're here, page cache is returned. The router is not executed. + return [ + 'matchedRoute' => [], + 'routes' => [], + ]; + } } } diff --git a/system/Debug/Toolbar/Collectors/Timers.php b/system/Debug/Toolbar/Collectors/Timers.php index cfce9e617594..163c9f5743f0 100644 --- a/system/Debug/Toolbar/Collectors/Timers.php +++ b/system/Debug/Toolbar/Collectors/Timers.php @@ -1,5 +1,7 @@ viewer ??= Services::renderer(); + $this->viewer ??= service('renderer'); } /** diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 03500456757e..8179c1a8e631 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -1,4 +1,4 @@ -setErrorMessage(lang('Email.attachmentMissing', [$file])); return false; @@ -748,7 +750,7 @@ public function setHeader($header, $value) protected function stringToArray($email) { if (! is_array($email)) { - return (strpos($email, ',') !== false) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email); + return (str_contains($email, ',')) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email); } return $email; @@ -872,7 +874,7 @@ protected function getEncoding() } foreach ($this->baseCharsets as $charset) { - if (strpos($this->charset, $charset) === 0) { + if (str_starts_with($this->charset, $charset)) { $this->encoding = '7bit'; break; @@ -907,7 +909,7 @@ protected function setDate() { $timezone = date('Z'); $operator = ($timezone[0] === '-') ? '-' : '+'; - $timezone = abs($timezone); + $timezone = abs((int) $timezone); $timezone = floor($timezone / 3600) * 100 + ($timezone % 3600) / 60; return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone); @@ -1021,7 +1023,7 @@ public function wordWrap($str, $charlim = null) $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars; } - if (strpos($str, "\r") !== false) { + if (str_contains($str, "\r")) { $str = str_replace(["\r\n", "\r"], "\n", $str); } @@ -1419,7 +1421,7 @@ protected function prepQuotedPrintable($str) $str = preg_replace(['| +|', '/\x00+/'], [' ', ''], $str); // Standardize newlines - if (strpos($str, "\r") !== false) { + if (str_contains($str, "\r")) { $str = str_replace(["\r\n", "\r"], "\n", $str); } @@ -1654,10 +1656,7 @@ protected function unwrapSpecials() { $this->finalBody = preg_replace_callback( '/\{unwrap\}(.*?)\{\/unwrap\}/si', - [ - $this, - 'removeNLCallback', - ], + $this->removeNLCallback(...), $this->finalBody ); } @@ -1673,7 +1672,7 @@ protected function unwrapSpecials() */ protected function removeNLCallback($matches) { - if (strpos($matches[1], "\r") !== false || strpos($matches[1], "\n") !== false) { + if (str_contains($matches[1], "\r") || str_contains($matches[1], "\n")) { $matches[1] = str_replace(["\r\n", "\r", "\n"], '', $matches[1]); } @@ -1854,7 +1853,7 @@ protected function sendWithSmtp() $this->setErrorMessage($reply); $this->SMTPEnd(); - if (strpos($reply, '250') !== 0) { + if (! str_starts_with($reply, '250')) { $this->setErrorMessage(lang('Email.SMTPError', [$reply])); return false; @@ -2026,11 +2025,11 @@ protected function SMTPAuthenticate() $this->sendData('AUTH LOGIN'); $reply = $this->getSMTPData(); - if (strpos($reply, '503') === 0) { // Already authenticated + if (str_starts_with($reply, '503')) { // Already authenticated return true; } - if (strpos($reply, '334') !== 0) { + if (! str_starts_with($reply, '334')) { $this->setErrorMessage(lang('Email.failedSMTPLogin', [$reply])); return false; @@ -2039,7 +2038,7 @@ protected function SMTPAuthenticate() $this->sendData(base64_encode($this->SMTPUser)); $reply = $this->getSMTPData(); - if (strpos($reply, '334') !== 0) { + if (! str_starts_with($reply, '334')) { $this->setErrorMessage(lang('Email.SMTPAuthUsername', [$reply])); return false; @@ -2048,7 +2047,7 @@ protected function SMTPAuthenticate() $this->sendData(base64_encode($this->SMTPPass)); $reply = $this->getSMTPData(); - if (strpos($reply, '235') !== 0) { + if (! str_starts_with($reply, '235')) { $this->setErrorMessage(lang('Email.SMTPAuthPassword', [$reply])); return false; diff --git a/system/Encryption/EncrypterInterface.php b/system/Encryption/EncrypterInterface.php index 161238177c49..2f40995041f3 100644 --- a/system/Encryption/EncrypterInterface.php +++ b/system/Encryption/EncrypterInterface.php @@ -1,5 +1,7 @@ [Entity] --- (2) --> [Database] + * [App Code] <-- (4) --- [Entity] <-- (3) --- [Database] */ interface CastInterface { /** - * Get + * Takes a raw value from Entity, returns its value for PHP. * * @param array|bool|float|int|object|string|null $value Data * @param array $params Additional param @@ -27,7 +33,7 @@ interface CastInterface public static function get($value, array $params = []); /** - * Set + * Takes a PHP value, returns its raw value for Entity. * * @param array|bool|float|int|object|string|null $value Data * @param array $params Additional param diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php index 423300cdae6f..2d01ad79b0ae 100644 --- a/system/Entity/Cast/DatetimeCast.php +++ b/system/Entity/Cast/DatetimeCast.php @@ -1,5 +1,7 @@ dataCaster = new DataCaster( + array_merge($this->defaultCastHandlers, $this->castHandlers), + null, + null, + false + ); + $this->syncOriginal(); $this->fill($data); @@ -167,7 +181,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu { $this->_cast = $cast; - $keys = array_filter(array_keys($this->attributes), static fn ($key) => strpos($key, '_') !== 0); + $keys = array_filter(array_keys($this->attributes), static fn ($key) => ! str_starts_with($key, '_')); if (is_array($this->datamap)) { $keys = array_unique( @@ -347,8 +361,8 @@ protected function mutateDate($value) /** * Provides the ability to cast an item as a specific data type. - * Add ? at the beginning of $type (i.e. ?string) to get NULL - * instead of casting $value if $value === null + * Add ? at the beginning of the type (i.e. ?string) to get `null` + * instead of casting $value when $value is null. * * @param bool|float|int|string|null $value Attribute value * @param string $attribute Attribute name @@ -360,58 +374,10 @@ protected function mutateDate($value) */ protected function castAs($value, string $attribute, string $method = 'get') { - if (empty($this->casts[$attribute])) { - return $value; - } - - $type = $this->casts[$attribute]; - - $isNullable = false; - - if (strpos($type, '?') === 0) { - $isNullable = true; - - if ($value === null) { - return null; - } - - $type = substr($type, 1); - } - - // In order not to create a separate handler for the - // json-array type, we transform the required one. - $type = $type === 'json-array' ? 'json[array]' : $type; - - if (! in_array($method, ['get', 'set'], true)) { - throw CastException::forInvalidMethod($method); - } - - $params = []; - - // Attempt to retrieve additional parameters if specified - // type[param, param2,param3] - if (preg_match('/^(.+)\[(.+)\]$/', $type, $matches)) { - $type = $matches[1]; - $params = array_map('trim', explode(',', $matches[2])); - } - - if ($isNullable) { - $params[] = 'nullable'; - } - - $type = trim($type, '[]'); - - $handlers = array_merge($this->defaultCastHandlers, $this->castHandlers); - - if (empty($handlers[$type])) { - return $value; - } - - if (! is_subclass_of($handlers[$type], CastInterface::class)) { - throw CastException::forInvalidInterface($handlers[$type]); - } - - return $handlers[$type]::$method($value, $params); + return $this->dataCaster + // @TODO if $casts is readonly, we don't need the setTypes() method. + ->setTypes($this->casts) + ->castAs($value, $attribute, $method); } /** diff --git a/system/Entity/Exceptions/CastException.php b/system/Entity/Exceptions/CastException.php index e259447b3f76..90d3885d9b62 100644 --- a/system/Entity/Exceptions/CastException.php +++ b/system/Entity/Exceptions/CastException.php @@ -1,5 +1,7 @@ new static(lang('Cast.jsonErrorDepth')), + JSON_ERROR_STATE_MISMATCH => new static(lang('Cast.jsonErrorStateMismatch')), + JSON_ERROR_CTRL_CHAR => new static(lang('Cast.jsonErrorCtrlChar')), + JSON_ERROR_SYNTAX => new static(lang('Cast.jsonErrorSyntax')), + JSON_ERROR_UTF8 => new static(lang('Cast.jsonErrorUtf8')), + default => new static(lang('Cast.jsonErrorUnknown')), + }; } /** diff --git a/system/Events/Events.php b/system/Events/Events.php index f756fa0ec0c4..a06bd7903e69 100644 --- a/system/Events/Events.php +++ b/system/Events/Events.php @@ -1,5 +1,7 @@ shouldDiscover('events')) { - $files = Services::locator()->search('Config/Events.php'); + $files = service('locator')->search('Config/Events.php'); } $files = array_filter(array_map(static function (string $file) { diff --git a/system/Exceptions/CastException.php b/system/Exceptions/CastException.php deleted file mode 100644 index 8dfb29543a78..000000000000 --- a/system/Exceptions/CastException.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Exceptions; - -/** - * Cast Exceptions. - * - * @deprecated use CodeIgniter\Entity\Exceptions\CastException instead. - * - * @codeCoverageIgnore - */ -class CastException extends CriticalError implements HasExitCodeInterface -{ - use DebugTraceableTrait; - - public function getExitCode(): int - { - return EXIT_CONFIG; - } - - /** - * @return static - */ - public static function forInvalidJsonFormatException(int $error) - { - switch ($error) { - case JSON_ERROR_DEPTH: - return new static(lang('Cast.jsonErrorDepth')); - - case JSON_ERROR_STATE_MISMATCH: - return new static(lang('Cast.jsonErrorStateMismatch')); - - case JSON_ERROR_CTRL_CHAR: - return new static(lang('Cast.jsonErrorCtrlChar')); - - case JSON_ERROR_SYNTAX: - return new static(lang('Cast.jsonErrorSyntax')); - - case JSON_ERROR_UTF8: - return new static(lang('Cast.jsonErrorUtf8')); - - default: - return new static(lang('Cast.jsonErrorUnknown')); - } - } -} diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php index 6eea2638c5e0..d8849b809c68 100644 --- a/system/Exceptions/ConfigException.php +++ b/system/Exceptions/ConfigException.php @@ -1,5 +1,7 @@ getSize() / 1024, 3); - - case 'mb': - return number_format(($this->getSize() / 1024) / 1024, 3); - - default: - return $this->getSize(); - } + return match (strtolower($unit)) { + 'kb' => number_format($this->getSize() / 1024, 3), + 'mb' => number_format(($this->getSize() / 1024) / 1024, 3), + default => $this->getSize(), + }; } /** @@ -173,7 +170,7 @@ public function getDestination(string $destination, string $delimiter = '_', int $info = pathinfo($destination); $extension = isset($info['extension']) ? '.' . $info['extension'] : ''; - if (strpos($info['filename'], $delimiter) !== false) { + if (str_contains($info['filename'], $delimiter)) { $parts = explode($delimiter, $info['filename']); if (is_numeric(end($parts))) { diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index 8e608e3ab311..b9456dcc15ba 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -1,5 +1,7 @@ strpos($value, $directory) === 0); + return array_filter($files, static fn (string $value): bool => str_starts_with($value, $directory)); } /** @@ -180,7 +182,7 @@ public function add($paths, bool $recursive = true) try { // Test for a directory self::resolveDirectory($path); - } catch (FileException $e) { + } catch (FileException) { $this->addFile($path); continue; diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php index 5145b5b089c6..90ccb9b501a0 100644 --- a/system/Filters/CSRF.php +++ b/system/Filters/CSRF.php @@ -1,5 +1,7 @@ verify($request); diff --git a/system/Filters/Cors.php b/system/Filters/Cors.php new file mode 100644 index 000000000000..93ca551b42f2 --- /dev/null +++ b/system/Filters/Cors.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\Cors as CorsService; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * @see \CodeIgniter\Filters\CorsTest + */ +class Cors implements FilterInterface +{ + private ?CorsService $cors = null; + + /** + * @testTag $config is used for testing purposes only. + * + * @param array{ + * allowedOrigins?: list, + * allowedOriginsPatterns?: list, + * supportsCredentials?: bool, + * allowedHeaders?: list, + * exposedHeaders?: list, + * allowedMethods?: list, + * maxAge?: int, + * } $config + */ + public function __construct(array $config = []) + { + if ($config !== []) { + $this->cors = new CorsService($config); + } + } + + /** + * @param list|null $arguments + * + * @return ResponseInterface|string|void + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return; + } + + $this->createCorsService($arguments); + + if (! $this->cors->isPreflightRequest($request)) { + return; + } + + /** @var ResponseInterface $response */ + $response = service('response'); + + $response = $this->cors->handlePreflightRequest($request, $response); + + // Always adds `Vary: Access-Control-Request-Method` header for cacheability. + // If there is an intermediate cache server such as a CDN, if a plain + // OPTIONS request is sent, it may be cached. But valid preflight requests + // have this header, so it will be cached separately. + $response->appendHeader('Vary', 'Access-Control-Request-Method'); + + return $response; + } + + /** + * @param list|null $arguments + */ + private function createCorsService(?array $arguments): void + { + $this->cors ??= ($arguments === null) ? CorsService::factory() + : CorsService::factory($arguments[0]); + } + + /** + * @param list|null $arguments + * + * @return ResponseInterface|void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return; + } + + $this->createCorsService($arguments); + + // Always adds `Vary: Access-Control-Request-Method` header for cacheability. + // If there is an intermediate cache server such as a CDN, if a plain + // OPTIONS request is sent, it may be cached. But valid preflight requests + // have this header, so it will be cached separately. + if ($request->is('OPTIONS')) { + $response->appendHeader('Vary', 'Access-Control-Request-Method'); + } + + return $this->cors->addResponseHeaders($request, $response); + } +} diff --git a/system/Filters/DebugToolbar.php b/system/Filters/DebugToolbar.php index b05a92b6640e..9f864ee572c5 100644 --- a/system/Filters/DebugToolbar.php +++ b/system/Filters/DebugToolbar.php @@ -1,5 +1,7 @@ prepare($request, $response); + service('toolbar')->prepare($request, $response); } } diff --git a/system/Filters/Exceptions/FilterException.php b/system/Filters/Exceptions/FilterException.php index 04bb36bdc2c5..1226ba318429 100644 --- a/system/Filters/Exceptions/FilterException.php +++ b/system/Filters/Exceptions/FilterException.php @@ -1,5 +1,7 @@ */ protected $filters = [ @@ -77,6 +82,8 @@ class Filters * The collection of filters' class names that will * be used to execute in each position. * + * This does not include "Required Filters". + * * @var array */ protected $filtersClass = [ @@ -128,7 +135,7 @@ public function __construct($config, RequestInterface $request, ResponseInterfac */ private function discoverFilters(): void { - $locator = Services::locator(); + $locator = service('locator'); // for access by custom filters $filters = $this->config; @@ -136,10 +143,11 @@ private function discoverFilters(): void $files = $locator->search('Config/Filters.php'); foreach ($files as $file) { + // The $file may not be a class file. $className = $locator->getClassname($file); // Don't include our main Filter config again... - if ($className === FiltersConfig::class) { + if ($className === FiltersConfig::class || $className === BaseFiltersConfig::class) { continue; } @@ -161,7 +169,8 @@ public function setResponse(ResponseInterface $response) * Runs through all of the filters for the specified * uri and position. * - * @param string $uri URI path relative to baseURL + * @param string $uri URI path relative to baseURL + * @phpstan-param 'before'|'after' $position * * @return RequestInterface|ResponseInterface|string|null * @@ -171,55 +180,185 @@ public function run(string $uri, string $position = 'before') { $this->initialize(strtolower($uri)); - foreach ($this->filtersClass[$position] as $className) { + if ($position === 'before') { + return $this->runBefore($this->filtersClass[$position]); + } + + // After + return $this->runAfter($this->filtersClass[$position]); + } + + /** + * @return RequestInterface|ResponseInterface|string + */ + private function runBefore(array $filterClasses) + { + foreach ($filterClasses as $className) { $class = new $className(); if (! $class instanceof FilterInterface) { - throw FilterException::forIncorrectInterface(get_class($class)); + throw FilterException::forIncorrectInterface($class::class); } - if ($position === 'before') { - $result = $class->before( - $this->request, - $this->argumentsClass[$className] ?? null - ); - - if ($result instanceof RequestInterface) { - $this->request = $result; + $result = $class->before( + $this->request, + $this->argumentsClass[$className] ?? null + ); - continue; - } + if ($result instanceof RequestInterface) { + $this->request = $result; - // If the response object was sent back, - // then send it and quit. - if ($result instanceof ResponseInterface) { - // short circuit - bypass any other filters - return $result; - } - // Ignore an empty result - if (empty($result)) { - continue; - } + continue; + } + // If the response object was sent back, + // then send it and quit. + if ($result instanceof ResponseInterface) { + // short circuit - bypass any other filters return $result; } - if ($position === 'after') { - $result = $class->after( - $this->request, - $this->response, - $this->argumentsClass[$className] ?? null - ); + // Ignore an empty result + if (empty($result)) { + continue; + } - if ($result instanceof ResponseInterface) { - $this->response = $result; + return $result; + } - continue; - } + return $this->request; + } + + private function runAfter(array $filterClasses): ResponseInterface + { + foreach ($filterClasses as $className) { + $class = new $className(); + + if (! $class instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($class::class); + } + + $result = $class->after( + $this->request, + $this->response, + $this->argumentsClass[$className] ?? null + ); + + if ($result instanceof ResponseInterface) { + $this->response = $result; + + continue; } } - return $position === 'before' ? $this->request : $this->response; + return $this->response; + } + + /** + * Runs "Required Filters" for the specified position. + * + * @return RequestInterface|ResponseInterface|string|null + * + * @phpstan-param 'before'|'after' $position + * + * @throws FilterException + * + * @internal + */ + public function runRequired(string $position = 'before') + { + [$filters, $aliases] = $this->getRequiredFilters($position); + + if ($filters === []) { + return $position === 'before' ? $this->request : $this->response; + } + + $filterClasses = []; + + foreach ($filters as $alias) { + if (is_array($aliases[$alias])) { + $filterClasses[$position] = array_merge($filterClasses[$position], $aliases[$alias]); + } else { + $filterClasses[$position][] = $aliases[$alias]; + } + } + + if ($position === 'before') { + return $this->runBefore($filterClasses[$position]); + } + + // After + return $this->runAfter($filterClasses[$position]); + } + + /** + * Returns "Required Filters" for the specified position. + * + * @phpstan-param 'before'|'after' $position + * + * @internal + */ + public function getRequiredFilters(string $position = 'before'): array + { + // For backward compatibility. For users who do not update Config\Filters. + if (! isset($this->config->required[$position])) { + $baseConfig = config(BaseFiltersConfig::class); // @phpstan-ignore-line + $filters = $baseConfig->required[$position]; + $aliases = $baseConfig->aliases; + } else { + $filters = $this->config->required[$position]; + $aliases = $this->config->aliases; + } + + if ($filters === []) { + return [[], $aliases]; + } + + if ($position === 'after') { + if (in_array('toolbar', $this->filters['after'], true)) { + // It was already run in globals filters. So remove it. + $filters = $this->setToolbarToLast($filters, true); + } else { + // Set the toolbar filter to the last position to be executed + $filters = $this->setToolbarToLast($filters); + } + } + + foreach ($filters as $alias) { + if (! array_key_exists($alias, $aliases)) { + throw FilterException::forNoAlias($alias); + } + } + + return [$filters, $aliases]; + } + + /** + * 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 + */ + private function setToolbarToLast(array $filters, bool $remove = false): array + { + $afters = []; + $found = false; + + foreach ($filters as $alias) { + if ($alias === 'toolbar') { + $found = true; + + continue; + } + + $afters[] = $alias; + } + + if ($found && ! $remove) { + $afters[] = 'toolbar'; + } + + return $afters; } /** @@ -237,6 +376,8 @@ public function run(string $uri, string $position = 'before') * * @param string|null $uri URI path relative to baseURL (all lowercase) * + * @TODO We don't need to accept null as $uri. + * * @return Filters */ public function initialize(?string $uri = null) @@ -246,20 +387,21 @@ public function initialize(?string $uri = null) } // Decode URL-encoded string - $uri = urldecode($uri); - - $this->processGlobals($uri); - $this->processMethods(); - $this->processFilters($uri); + $uri = urldecode($uri ?? ''); + + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if ($oldFilterOrder) { + $this->processGlobals($uri); + $this->processMethods(); + $this->processFilters($uri); + } else { + $this->processFilters($uri); + $this->processMethods(); + $this->processGlobals($uri); + } // Set the toolbar filter to the last position to be executed - if (in_array('toolbar', $this->filters['after'], true) - && ($count = count($this->filters['after'])) > 1 - && $this->filters['after'][$count - 1] !== 'toolbar' - ) { - array_splice($this->filters['after'], array_search('toolbar', $this->filters['after'], true), 1); - $this->filters['after'][] = 'toolbar'; - } + $this->filters['after'] = $this->setToolbarToLast($this->filters['after']); $this->processAliasesToClass('before'); $this->processAliasesToClass('after'); @@ -290,6 +432,7 @@ public function reset(): self /** * Returns the processed filters array. + * This does not include "Required Filters". */ public function getFilters(): array { @@ -298,6 +441,7 @@ public function getFilters(): array /** * Returns the filtersClass array. + * This does not include "Required Filters". */ public function getFiltersClass(): array { @@ -338,12 +482,8 @@ public function addFilter(string $class, ?string $alias = null, string $when = ' * are passed to the filter when executed. * * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' - * - * @return $this - * - * @deprecated Use enableFilters(). This method will be private. */ - public function enableFilter(string $name, string $when = 'before') + private function enableFilter(string $name, string $when = 'before'): void { // Get arguments and clean name [$name, $arguments] = $this->getCleanName($name); @@ -365,8 +505,6 @@ public function enableFilter(string $name, string $when = 'before') $this->filters[$when][] = $name; $this->filtersClass[$when] = array_merge($this->filtersClass[$when], $classNames); } - - return $this; } /** @@ -380,7 +518,7 @@ private function getCleanName(string $name): array { $arguments = []; - if (strpos($name, ':') !== false) { + if (str_contains($name, ':')) { [$name, $arguments] = explode(':', $name); $arguments = explode(',', $arguments); @@ -444,6 +582,8 @@ protected function processGlobals(?string $uri = null) // Add any global filters, unless they are excluded for this URI $sets = ['before', 'after']; + $filters = []; + foreach ($sets as $set) { if (isset($this->config->globals[$set])) { // look at each alias in the group @@ -463,11 +603,24 @@ protected function processGlobals(?string $uri = null) } if ($keep) { - $this->filters[$set][] = $alias; + $filters[$set][] = $alias; } } } } + + if (isset($filters['before'])) { + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if ($oldFilterOrder) { + $this->filters['before'] = array_merge($this->filters['before'], $filters['before']); + } else { + $this->filters['before'] = array_merge($filters['before'], $this->filters['before']); + } + } + + if (isset($filters['after'])) { + $this->filters['after'] = array_merge($this->filters['after'], $filters['after']); + } } /** @@ -481,11 +634,34 @@ protected function processMethods() return; } - // Request method won't be set for CLI-based requests - $method = strtolower($this->request->getMethod()) ?? 'cli'; + $method = $this->request->getMethod(); + + $found = false; if (array_key_exists($method, $this->config->methods)) { - $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]); + $found = true; + } + // Checks lowercase HTTP method for backward compatibility. + // @deprecated 4.5.0 + // @TODO remove this in the future. + elseif (array_key_exists(strtolower($method), $this->config->methods)) { + @trigger_error( + 'Setting lowercase HTTP method key "' . strtolower($method) . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($method) . '".', + E_USER_DEPRECATED + ); + + $found = true; + $method = strtolower($method); + } + + if ($found) { + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if ($oldFilterOrder) { + $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]); + } else { + $this->filters['before'] = array_merge($this->config->methods[$method], $this->filters['before']); + } } } @@ -505,6 +681,8 @@ protected function processFilters(?string $uri = null) $uri = strtolower(trim($uri, '/ ')); // Add any filters that apply to this URI + $filters = []; + foreach ($this->config->filters as $alias => $settings) { // Look for inclusion rules if (isset($settings['before'])) { @@ -514,7 +692,7 @@ protected function processFilters(?string $uri = null) // Get arguments and clean name [$name, $arguments] = $this->getCleanName($alias); - $this->filters['before'][] = $name; + $filters['before'][] = $name; $this->registerArguments($name, $arguments); } @@ -527,7 +705,7 @@ protected function processFilters(?string $uri = null) // Get arguments and clean name [$name, $arguments] = $this->getCleanName($alias); - $this->filters['after'][] = $name; + $filters['after'][] = $name; // The arguments may have already been registered in the before filter. // So disable check. @@ -535,6 +713,24 @@ protected function processFilters(?string $uri = null) } } } + + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + + if (isset($filters['before'])) { + if ($oldFilterOrder) { + $this->filters['before'] = array_merge($this->filters['before'], $filters['before']); + } else { + $this->filters['before'] = array_merge($filters['before'], $this->filters['before']); + } + } + + if (isset($filters['after'])) { + if (! $oldFilterOrder) { + $filters['after'] = array_reverse($filters['after']); + } + + $this->filters['after'] = array_merge($this->filters['after'], $filters['after']); + } } /** @@ -571,6 +767,8 @@ private function registerArguments(string $name, array $arguments, bool $check = */ protected function processAliasesToClass(string $position) { + $filterClasses = []; + foreach ($this->filters[$position] as $alias => $rules) { if (is_numeric($alias) && is_string($rules)) { $alias = $rules; @@ -581,14 +779,20 @@ protected function processAliasesToClass(string $position) } if (is_array($this->config->aliases[$alias])) { - $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $this->config->aliases[$alias]); + $filterClasses = [...$filterClasses, ...$this->config->aliases[$alias]]; } else { - $this->filtersClass[$position][] = $this->config->aliases[$alias]; + $filterClasses[] = $this->config->aliases[$alias]; } } - // when using enableFilter() we already write the class name in $filtersClass as well as the + // when using enableFilter() we already write the class name in $filterClasses as well as the // alias in $filters. This leads to duplicates when using route filters. + if ($position === 'before') { + $this->filtersClass[$position] = array_merge($filterClasses, $this->filtersClass[$position]); + } else { + $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClasses); + } + // Since some filters like rate limiters rely on being executed once a request we filter em here. $this->filtersClass[$position] = array_values(array_unique($this->filtersClass[$position])); } @@ -604,7 +808,7 @@ protected function processAliasesToClass(string $position) private function pathApplies(string $uri, $paths) { // empty path matches all - if (empty($paths)) { + if ($paths === '' || $paths === []) { return true; } diff --git a/system/Filters/ForceHTTPS.php b/system/Filters/ForceHTTPS.php new file mode 100644 index 000000000000..f415bd00e3b8 --- /dev/null +++ b/system/Filters/ForceHTTPS.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\Exceptions\RedirectException; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\App; + +/** + * Force HTTPS filter + */ +class ForceHTTPS implements FilterInterface +{ + /** + * Force Secure Site Access? If the config value 'forceGlobalSecureRequests' + * is true, will enforce that all requests to this site are made through + * HTTPS. Will redirect the user to the current page with HTTPS, as well + * as set the HTTP Strict Transport Security (HSTS) header for those browsers + * that support it. + * + * @param array|null $arguments + * + * @return ResponseInterface|void + */ + public function before(RequestInterface $request, $arguments = null) + { + $config = config(App::class); + + if ($config->forceGlobalSecureRequests !== true) { + return; + } + + $response = service('response'); + + try { + force_https(YEAR, $request, $response); + } catch (RedirectException $e) { + return $e->getResponse(); + } + } + + /** + * We don't have anything to do here. + * + * @param array|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + } +} diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php index bd2dbf44920e..c2fb98c62d60 100644 --- a/system/Filters/Honeypot.php +++ b/system/Filters/Honeypot.php @@ -1,5 +1,7 @@ attachHoneypot($response); + service('honeypot')->attachHoneypot($response); } } diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 5528c880369e..542b12d7ffbe 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -1,5 +1,7 @@ checkEncoding(...), $value); return $value; } @@ -112,7 +114,7 @@ protected function checkEncoding($value) protected function checkControl($value) { if (is_array($value)) { - array_map([$this, 'checkControl'], $value); + array_map($this->checkControl(...), $value); return $value; } diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php new file mode 100644 index 000000000000..a3d3af832a65 --- /dev/null +++ b/system/Filters/PageCache.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\Cache\ResponseCache; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\DownloadResponse; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Page Cache filter + */ +class PageCache implements FilterInterface +{ + private readonly ResponseCache $pageCache; + + public function __construct() + { + $this->pageCache = service('responsecache'); + } + + /** + * Checks page cache and return if found. + * + * @param array|null $arguments + * + * @return ResponseInterface|void + */ + public function before(RequestInterface $request, $arguments = null) + { + assert($request instanceof CLIRequest || $request instanceof IncomingRequest); + + $response = service('response'); + + $cachedResponse = $this->pageCache->get($request, $response); + + if ($cachedResponse instanceof ResponseInterface) { + return $cachedResponse; + } + } + + /** + * Cache the page. + * + * @param array|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + assert($request instanceof CLIRequest || $request instanceof IncomingRequest); + + if ( + ! $response instanceof DownloadResponse + && ! $response instanceof RedirectResponse + ) { + // Cache it without the performance metrics replaced + // so that we can have live speed updates along the way. + // Must be run after filters to preserve the Response headers. + $this->pageCache->make($request, $response); + } + } +} diff --git a/system/Filters/PerformanceMetrics.php b/system/Filters/PerformanceMetrics.php new file mode 100644 index 000000000000..f2371c7a8d6d --- /dev/null +++ b/system/Filters/PerformanceMetrics.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Performance Metrics filter + */ +class PerformanceMetrics implements FilterInterface +{ + /** + * We don't need to do anything here. + * + * @param array|null $arguments + */ + public function before(RequestInterface $request, $arguments = null) + { + } + + /** + * Replaces the performance metrics. + * + * @param array|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + $body = $response->getBody(); + + if ($body !== null) { + $benchmark = service('timer'); + + $output = str_replace( + [ + '{elapsed_time}', + '{memory_usage}', + ], + [ + (string) $benchmark->getElapsedTime('total_execution'), + number_format(memory_get_peak_usage() / 1024 / 1024, 3), + ], + $body + ); + + $response->setBody($output); + } + } +} diff --git a/system/Filters/SecureHeaders.php b/system/Filters/SecureHeaders.php index cf557069193e..b8bd6e05f8eb 100644 --- a/system/Filters/SecureHeaders.php +++ b/system/Filters/SecureHeaders.php @@ -1,5 +1,7 @@ responseOrig = $response ?? new Response(config(App::class)); $this->baseURI = $uri->useRawQueryString(); @@ -130,7 +132,7 @@ 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 + * @param string $method HTTP method */ public function request($method, string $url, array $options = []): ResponseInterface { @@ -177,7 +179,7 @@ protected function resetOptions() */ public function get(string $url, array $options = []): ResponseInterface { - return $this->request('get', $url, $options); + return $this->request(Method::GET, $url, $options); } /** @@ -185,7 +187,7 @@ public function get(string $url, array $options = []): ResponseInterface */ public function delete(string $url, array $options = []): ResponseInterface { - return $this->request('delete', $url, $options); + return $this->request('DELETE', $url, $options); } /** @@ -193,7 +195,7 @@ public function delete(string $url, array $options = []): ResponseInterface */ public function head(string $url, array $options = []): ResponseInterface { - return $this->request('head', $url, $options); + return $this->request('HEAD', $url, $options); } /** @@ -201,7 +203,7 @@ public function head(string $url, array $options = []): ResponseInterface */ public function options(string $url, array $options = []): ResponseInterface { - return $this->request('options', $url, $options); + return $this->request('OPTIONS', $url, $options); } /** @@ -209,7 +211,7 @@ public function options(string $url, array $options = []): ResponseInterface */ public function patch(string $url, array $options = []): ResponseInterface { - return $this->request('patch', $url, $options); + return $this->request('PATCH', $url, $options); } /** @@ -217,7 +219,7 @@ public function patch(string $url, array $options = []): ResponseInterface */ public function post(string $url, array $options = []): ResponseInterface { - return $this->request('post', $url, $options); + return $this->request(Method::POST, $url, $options); } /** @@ -225,7 +227,7 @@ public function post(string $url, array $options = []): ResponseInterface */ public function put(string $url, array $options = []): ResponseInterface { - return $this->request('put', $url, $options); + return $this->request(Method::PUT, $url, $options); } /** @@ -323,7 +325,7 @@ protected function parseOptions(array $options) protected function prepareURL(string $url): string { // If it's a full URI, then we have nothing to do here... - if (strpos($url, '://') !== false) { + if (str_contains($url, '://')) { return $url; } @@ -339,17 +341,6 @@ protected function prepareURL(string $url): string ); } - /** - * Get the request method. Overrides the Request class' method - * since users expect a different answer here. - * - * @param bool|false $upper Whether to return in upper or lower case. - */ - public function getMethod(bool $upper = false): string - { - return ($upper) ? strtoupper($this->method) : strtolower($this->method); - } - /** * Fires the actual cURL request. * @@ -389,16 +380,16 @@ public function send(string $method, string $url) // Set the string we want to break our response from $breakString = "\r\n\r\n"; - while (strpos($output, 'HTTP/1.1 100 Continue') === 0) { + while (str_starts_with($output, 'HTTP/1.1 100 Continue')) { $output = substr($output, strpos($output, $breakString) + 4); } - if (strpos($output, 'HTTP/1.1 200 Connection established') === 0) { + if (str_starts_with($output, 'HTTP/1.1 200 Connection established')) { $output = substr($output, strpos($output, $breakString) + 4); } // If request and response have Digest - if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && strpos($output, 'WWW-Authenticate: Digest') !== false) { + if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) { $output = substr($output, strpos($output, $breakString) + 4); } @@ -446,8 +437,6 @@ protected function applyRequestHeaders(array $curlOptions = []): array */ protected function applyMethod(string $method, array $curlOptions): array { - $method = strtoupper($method); - $this->method = $method; $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; @@ -458,7 +447,7 @@ protected function applyMethod(string $method, array $curlOptions): array return $this->applyBody($curlOptions); } - if ($method === 'PUT' || $method === 'POST') { + if ($method === Method::PUT || $method === Method::POST) { // See http://tools.ietf.org/html/rfc7230#section-3.3.2 if ($this->header('content-length') === null && ! isset($this->config['multipart'])) { $this->setHeader('Content-Length', '0'); @@ -492,11 +481,15 @@ protected function setResponseHeaders(array $headers = []) { foreach ($headers as $header) { if (($pos = strpos($header, ':')) !== false) { - $title = substr($header, 0, $pos); - $value = substr($header, $pos + 1); + $title = trim(substr($header, 0, $pos)); + $value = trim(substr($header, $pos + 1)); - $this->response->setHeader($title, $value); - } elseif (strpos($header, 'HTTP') === 0) { + if ($this->response instanceof Response) { + $this->response->addHeader($title, $value); + } else { + $this->response->setHeader($title, $value); + } + } elseif (str_starts_with($header, 'HTTP')) { preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches); if (isset($matches[1])) { diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index 83603b3a4a81..945c3e08d3ce 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -1,5 +1,7 @@ + */ + protected array $directives = [ + 'base-uri' => 'baseURI', + 'child-src' => 'childSrc', + 'connect-src' => 'connectSrc', + 'default-src' => 'defaultSrc', + 'font-src' => 'fontSrc', + 'form-action' => 'formAction', + 'frame-ancestors' => 'frameAncestors', + 'frame-src' => 'frameSrc', + 'img-src' => 'imageSrc', + 'media-src' => 'mediaSrc', + 'object-src' => 'objectSrc', + 'plugin-types' => 'pluginTypes', + 'script-src' => 'scriptSrc', + 'style-src' => 'styleSrc', + 'manifest-src' => 'manifestSrc', + 'sandbox' => 'sandbox', + 'report-uri' => 'reportURI', + ]; + /** * Used for security enforcement * @@ -113,37 +140,37 @@ class ContentSecurityPolicy /** * Used for security enforcement * - * @var string + * @var array|string */ - protected $reportURI; + protected $scriptSrc = []; /** * Used for security enforcement * * @var array|string */ - protected $sandbox = []; + protected $styleSrc = []; /** * Used for security enforcement * * @var array|string */ - protected $scriptSrc = []; + protected $manifestSrc = []; /** * Used for security enforcement * * @var array|string */ - protected $styleSrc = []; + protected $sandbox = []; /** * Used for security enforcement * - * @var array|string + * @var string|null */ - protected $manifestSrc = []; + protected $reportURI; /** * Used for security enforcement @@ -704,26 +731,6 @@ protected function buildHeaders(ResponseInterface $response) $response->setHeader('Content-Security-Policy', []); $response->setHeader('Content-Security-Policy-Report-Only', []); - $directives = [ - 'base-uri' => 'baseURI', - 'child-src' => 'childSrc', - 'connect-src' => 'connectSrc', - 'default-src' => 'defaultSrc', - 'font-src' => 'fontSrc', - 'form-action' => 'formAction', - 'frame-ancestors' => 'frameAncestors', - 'frame-src' => 'frameSrc', - 'img-src' => 'imageSrc', - 'media-src' => 'mediaSrc', - 'object-src' => 'objectSrc', - 'plugin-types' => 'pluginTypes', - 'script-src' => 'scriptSrc', - 'style-src' => 'styleSrc', - 'manifest-src' => 'manifestSrc', - 'sandbox' => 'sandbox', - 'report-uri' => 'reportURI', - ]; - // inject default base & default URIs if needed if (empty($this->baseURI)) { $this->baseURI = 'self'; @@ -733,7 +740,7 @@ protected function buildHeaders(ResponseInterface $response) $this->defaultSrc = 'self'; } - foreach ($directives as $name => $property) { + foreach ($this->directives as $name => $property) { if (! empty($this->{$property})) { $this->addToHeader($name, $this->{$property}); } @@ -795,7 +802,7 @@ protected function addToHeader(string $name, $values = null) $reportOnly = $this->reportOnly; } - if (strpos($value, 'nonce-') === 0) { + if (str_starts_with($value, 'nonce-')) { $value = "'{$value}'"; } @@ -814,4 +821,20 @@ protected function addToHeader(string $name, $values = null) $this->reportOnlyHeaders[$name] = implode(' ', $reportSources); } } + + /** + * Clear the directive. + * + * @param string $directive CSP directive + */ + public function clearDirective(string $directive): void + { + if ($directive === 'report-uris') { + $this->{$this->directives[$directive]} = null; + + return; + } + + $this->{$this->directives[$directive]} = []; + } } diff --git a/system/HTTP/Cors.php b/system/HTTP/Cors.php new file mode 100644 index 000000000000..f7619c9f83a4 --- /dev/null +++ b/system/HTTP/Cors.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Exceptions\ConfigException; +use Config\Cors as CorsConfig; + +/** + * Cross-Origin Resource Sharing (CORS) + * + * @see \CodeIgniter\HTTP\CorsTest + */ +class Cors +{ + /** + * @var array{ + * allowedOrigins: list, + * allowedOriginsPatterns: list, + * supportsCredentials: bool, + * allowedHeaders: list, + * exposedHeaders: list, + * allowedMethods: list, + * maxAge: int, + * } + */ + private array $config = [ + 'allowedOrigins' => [], + 'allowedOriginsPatterns' => [], + 'supportsCredentials' => false, + 'allowedHeaders' => [], + 'exposedHeaders' => [], + 'allowedMethods' => [], + 'maxAge' => 7200, + ]; + + /** + * @param array{ + * allowedOrigins?: list, + * allowedOriginsPatterns?: list, + * supportsCredentials?: bool, + * allowedHeaders?: list, + * exposedHeaders?: list, + * allowedMethods?: list, + * maxAge?: int, + * }|CorsConfig|null $config + */ + public function __construct($config = null) + { + $config ??= config(CorsConfig::class); + if ($config instanceof CorsConfig) { + $config = $config->default; + } + $this->config = array_merge($this->config, $config); + } + + /** + * Creates a new instance by config name. + */ + public static function factory(string $configName = 'default'): self + { + $config = config(CorsConfig::class)->{$configName}; + + return new self($config); + } + + /** + * Whether if the request is a preflight request. + */ + public function isPreflightRequest(IncomingRequest $request): bool + { + return $request->is('OPTIONS') + && $request->hasHeader('Access-Control-Request-Method'); + } + + /** + * Handles the preflight request, and returns the response. + */ + public function handlePreflightRequest(RequestInterface $request, ResponseInterface $response): ResponseInterface + { + $response->setStatusCode(204); + + $this->setAllowOrigin($request, $response); + + if ($response->hasHeader('Access-Control-Allow-Origin')) { + $this->setAllowHeaders($response); + $this->setAllowMethods($response); + $this->setAllowMaxAge($response); + $this->setAllowCredentials($response); + } + + return $response; + } + + private function checkWildcard(string $name, int $count): void + { + if (in_array('*', $this->config[$name], true) && $count > 1) { + throw new ConfigException( + "If wildcard is specified, you must set `'{$name}' => ['*']`." + . ' But using wildcard is not recommended.' + ); + } + } + + private function checkWildcardAndCredentials(string $name, string $header): void + { + if ( + $this->config[$name] === ['*'] + && $this->config['supportsCredentials'] + ) { + throw new ConfigException( + 'When responding to a credentialed request, ' + . 'the server must not specify the "*" wildcard for the ' + . $header . ' response-header value.' + ); + } + } + + private function setAllowOrigin(RequestInterface $request, ResponseInterface $response): void + { + $originCount = count($this->config['allowedOrigins']); + $originPatternCount = count($this->config['allowedOriginsPatterns']); + + $this->checkWildcard('allowedOrigins', $originCount); + $this->checkWildcardAndCredentials('allowedOrigins', 'Access-Control-Allow-Origin'); + + // Single Origin. + if ($originCount === 1 && $originPatternCount === 0) { + $response->setHeader('Access-Control-Allow-Origin', $this->config['allowedOrigins'][0]); + + return; + } + + // Multiple Origins. + if (! $request->hasHeader('Origin')) { + return; + } + + $origin = $request->getHeaderLine('Origin'); + + if ($originCount > 1 && in_array($origin, $this->config['allowedOrigins'], true)) { + $response->setHeader('Access-Control-Allow-Origin', $origin); + $response->appendHeader('Vary', 'Origin'); + + return; + } + + if ($originPatternCount > 0) { + foreach ($this->config['allowedOriginsPatterns'] as $pattern) { + $regex = '#\A' . $pattern . '\z#'; + + if (preg_match($regex, $origin)) { + $response->setHeader('Access-Control-Allow-Origin', $origin); + $response->appendHeader('Vary', 'Origin'); + + return; + } + } + } + } + + private function setAllowHeaders(ResponseInterface $response): void + { + $this->checkWildcard('allowedHeaders', count($this->config['allowedHeaders'])); + $this->checkWildcardAndCredentials('allowedHeaders', 'Access-Control-Allow-Headers'); + + $response->setHeader( + 'Access-Control-Allow-Headers', + implode(', ', $this->config['allowedHeaders']) + ); + } + + private function setAllowMethods(ResponseInterface $response): void + { + $this->checkWildcard('allowedMethods', count($this->config['allowedMethods'])); + $this->checkWildcardAndCredentials('allowedMethods', 'Access-Control-Allow-Methods'); + + $response->setHeader( + 'Access-Control-Allow-Methods', + implode(', ', $this->config['allowedMethods']) + ); + } + + private function setAllowMaxAge(ResponseInterface $response): void + { + $response->setHeader('Access-Control-Max-Age', (string) $this->config['maxAge']); + } + + private function setAllowCredentials(ResponseInterface $response): void + { + if ($this->config['supportsCredentials']) { + $response->setHeader('Access-Control-Allow-Credentials', 'true'); + } + } + + /** + * Adds CORS headers to the Response. + */ + public function addResponseHeaders(RequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->setAllowOrigin($request, $response); + + if ($response->hasHeader('Access-Control-Allow-Origin')) { + $this->setAllowCredentials($response); + $this->setExposeHeaders($response); + } + + return $response; + } + + private function setExposeHeaders(ResponseInterface $response): void + { + if ($this->config['exposedHeaders'] !== []) { + $response->setHeader( + 'Access-Control-Expose-Headers', + implode(', ', $this->config['exposedHeaders']) + ); + } + } +} diff --git a/system/HTTP/DownloadResponse.php b/system/HTTP/DownloadResponse.php index d815a9af6e03..6b293fbca239 100644 --- a/system/HTTP/DownloadResponse.php +++ b/system/HTTP/DownloadResponse.php @@ -1,5 +1,7 @@ response) { - $this->response = Services::response() + $this->response = service('response') ->redirect(base_url($this->getMessage()), 'auto', $this->getCode()); } - Services::logger()->info( + service('logger')->info( 'REDIRECTED ROUTE at ' - . ($this->response->getHeaderLine('Location') ?: substr($this->response->getHeaderLine('Refresh'), 6)) + . ($this->response->getHeaderLine('Location') ?: substr($this->response->getHeaderLine('Refresh'), 6)) ); return $this->response; diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index 2ce05e2c3a4c..079d5e73114f 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -1,5 +1,7 @@ populateFiles(); if ($this->hasFile($name)) { - if (strpos($name, '.') !== false) { + if (str_contains($name, '.')) { $name = explode('.', $name); $uploadedFile = $this->getValueDotNotationSyntax($name, $this->files); @@ -84,7 +86,7 @@ public function getFileMultiple(string $name) $this->populateFiles(); if ($this->hasFile($name)) { - if (strpos($name, '.') !== false) { + if (str_contains($name, '.')) { $name = explode('.', $name); $uploadedFile = $this->getValueDotNotationSyntax($name, $this->files); @@ -113,7 +115,7 @@ public function hasFile(string $fileID): bool { $this->populateFiles(); - if (strpos($fileID, '.') !== false) { + if (str_contains($fileID, '.')) { $segments = explode('.', $fileID); $el = $this->files; @@ -185,7 +187,7 @@ protected function createFileObject(array $array) $array['tmp_name'] ?? null, $array['name'] ?? null, $array['type'] ?? null, - $array['size'] ?? null, + ($array['size'] ?? null) === null ? null : (int) $array['size'], $array['error'] ?? null, $array['full_path'] ?? null ); diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index e0725e7c90d5..78643aa7166e 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -1,5 +1,7 @@ hasMoved = move_uploaded_file($this->path, $destination); - } catch (Exception $e) { + } catch (Exception) { $error = error_get_last(); $message = strip_tags($error['message'] ?? ''); diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index ef2073f2b876..12cc25a0d050 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -1,5 +1,7 @@ |string>|string */ diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 499fa0b1cf06..ec13e40f6ff5 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -1,5 +1,7 @@ getHeaderLine('Content-Type'), 'multipart/form-data') === false + && ! str_contains($this->getHeaderLine('Content-Type'), 'multipart/form-data') && (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize() ) { // Get our body from php://input @@ -174,7 +153,6 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U $body = null; } - $this->config = $config; $this->uri = $uri; $this->body = $body; $this->userAgent = $userAgent; @@ -195,24 +173,12 @@ private function getPostMaxSize(): int { $postMaxSize = ini_get('post_max_size'); - switch (strtoupper(substr($postMaxSize, -1))) { - case 'G': - $postMaxSize = (int) str_replace('G', '', $postMaxSize) * 1024 ** 3; - break; - - case 'M': - $postMaxSize = (int) str_replace('M', '', $postMaxSize) * 1024 ** 2; - break; - - case 'K': - $postMaxSize = (int) str_replace('K', '', $postMaxSize) * 1024; - break; - - default: - $postMaxSize = (int) $postMaxSize; - } - - return $postMaxSize; + return match (strtoupper(substr($postMaxSize, -1))) { + 'G' => (int) str_replace('G', '', $postMaxSize) * 1024 ** 3, + 'M' => (int) str_replace('M', '', $postMaxSize) * 1024 ** 2, + 'K' => (int) str_replace('K', '', $postMaxSize) * 1024, + default => (int) $postMaxSize, + }; } /** @@ -260,20 +226,11 @@ public function detectPath(string $protocol = ''): string $protocol = 'REQUEST_URI'; } - switch ($protocol) { - case 'REQUEST_URI': - $this->path = $this->parseRequestURI(); - break; - - case 'QUERY_STRING': - $this->path = $this->parseQueryString(); - break; - - case 'PATH_INFO': - default: - $this->path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI(); - break; - } + $this->path = match ($protocol) { + 'REQUEST_URI' => $this->parseRequestURI(), + 'QUERY_STRING' => $this->parseQueryString(), + default => $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI(), + }; return $this->path; } @@ -321,7 +278,7 @@ protected function parseRequestURI(): string // This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct // URI is found, and also fixes the QUERY_STRING Server var and $_GET array. - if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) { + if (trim($uri, '/') === '' && str_starts_with($query, '/')) { $query = explode('?', $query, 2); $uri = $query[0]; $_SERVER['QUERY_STRING'] = $query[1] ?? ''; @@ -354,7 +311,7 @@ protected function parseQueryString(): string return '/'; } - if (strncmp($uri, '/', 1) === 0) { + if (str_starts_with($uri, '/')) { $uri = explode('?', $uri, 2); $_SERVER['QUERY_STRING'] = $uri[1] ?? ''; $uri = $uri[0]; @@ -380,21 +337,13 @@ public function negotiate(string $type, array $supported, bool $strictMatch = fa $this->negotiator = Services::negotiator($this, true); } - switch (strtolower($type)) { - case 'media': - return $this->negotiator->media($supported, $strictMatch); - - case 'charset': - return $this->negotiator->charset($supported); - - case 'encoding': - return $this->negotiator->encoding($supported); - - case 'language': - return $this->negotiator->language($supported); - } - - throw HTTPException::forInvalidNegotiationType($type); + return match (strtolower($type)) { + 'media' => $this->negotiator->media($supported, $strictMatch), + 'charset' => $this->negotiator->charset($supported), + 'encoding' => $this->negotiator->encoding($supported), + 'language' => $this->negotiator->language($supported), + default => throw HTTPException::forInvalidNegotiationType($type), + }; } /** @@ -407,14 +356,14 @@ public function is(string $type): bool { $valueUpper = strtoupper($type); - $httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS']; + $httpMethods = Method::all(); if (in_array($valueUpper, $httpMethods, true)) { - return strtoupper($this->getMethod()) === $valueUpper; + return $this->getMethod() === $valueUpper; } if ($valueUpper === 'JSON') { - return strpos($this->getHeaderLine('Content-Type'), 'application/json') !== false; + return str_contains($this->getHeaderLine('Content-Type'), 'application/json'); } if ($valueUpper === 'AJAX') { @@ -550,7 +499,7 @@ public function getDefaultLocale(): string public function getVar($index = null, $filter = null, $flags = null) { if ( - strpos($this->getHeaderLine('Content-Type'), 'application/json') !== false + str_contains($this->getHeaderLine('Content-Type'), 'application/json') && $this->body !== null ) { return $this->getJsonVar($index, false, $filter, $flags); @@ -931,18 +880,4 @@ public function getFile(string $fileID) return $this->files->getFile($fileID); } - - /** - * Remove relative directory (../) and multi slashes (///) - * - * Do some final cleaning of the URI and return it, currently only used in static::_parse_request_uri() - * - * @deprecated 4.1.2 Use URI::removeDotSegments() directly - */ - protected function removeRelativeDirectory(string $uri): string - { - $uri = URI::removeDotSegments($uri); - - return $uri === '/' ? $uri : ltrim($uri, '/'); - } } diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 9d0b517778a4..71c4429f28ee 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -1,5 +1,7 @@ hasMultipleHeaders($name)) { + throw new InvalidArgumentException( + 'The header "' . $name . '" already has multiple headers.' + . ' You cannot use getHeaderLine().' + ); + } + $origName = $this->getHeaderName($name); if (! array_key_exists($origName, $this->headers)) { diff --git a/system/HTTP/MessageInterface.php b/system/HTTP/MessageInterface.php index 99867bde5800..b8850a0ffb7c 100644 --- a/system/HTTP/MessageInterface.php +++ b/system/HTTP/MessageInterface.php @@ -1,5 +1,7 @@ An array of the Header objects + * @return array> An array of the Header objects */ public function headers(): array; @@ -83,7 +85,7 @@ public function hasHeader(string $name): bool; * * @param string $name * - * @return array|Header|null + * @return Header|list
|null */ public function header($name); diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index ac3e5b18938c..2f6e57a90c57 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -1,5 +1,7 @@ + * [name => Header] + * or + * [name => [Header1, Header2]] + * + * @var array> */ protected $headers = []; @@ -93,7 +100,7 @@ public function populateHeaders(): void $this->setHeader($header, $_SERVER[$key]); - // Add us to the header map so we can find them case-insensitively + // Add us to the header map, so we can find them case-insensitively $this->headerMap[strtolower($header)] = $header; } } @@ -102,7 +109,7 @@ public function populateHeaders(): void /** * Returns an array containing all Headers. * - * @return array An array of the Header objects + * @return array> An array of the Header objects */ public function headers(): array { @@ -122,7 +129,7 @@ public function headers(): array * * @param string $name * - * @return array|Header|null + * @return Header|list
|null */ public function header($name) { @@ -140,9 +147,14 @@ public function header($name) */ public function setHeader(string $name, $value): self { + $this->checkMultipleHeaders($name); + $origName = $this->getHeaderName($name); - if (isset($this->headers[$origName]) && is_array($this->headers[$origName]->getValue())) { + if ( + isset($this->headers[$origName]) + && is_array($this->headers[$origName]->getValue()) + ) { if (! is_array($value)) { $value = [$value]; } @@ -158,6 +170,23 @@ public function setHeader(string $name, $value): self return $this; } + private function hasMultipleHeaders(string $name): bool + { + $origName = $this->getHeaderName($name); + + return isset($this->headers[$origName]) && is_array($this->headers[$origName]); + } + + private function checkMultipleHeaders(string $name): void + { + if ($this->hasMultipleHeaders($name)) { + throw new InvalidArgumentException( + 'The header "' . $name . '" already has multiple headers.' + . ' You cannot change them. If you really need to change, remove the header first.' + ); + } + } + /** * Removes a header from the list of headers we track. * @@ -179,6 +208,8 @@ public function removeHeader(string $name): self */ public function appendHeader(string $name, ?string $value): self { + $this->checkMultipleHeaders($name); + $origName = $this->getHeaderName($name); array_key_exists($origName, $this->headers) @@ -188,6 +219,33 @@ public function appendHeader(string $name, ?string $value): self return $this; } + /** + * 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 + { + $origName = $this->getHeaderName($name); + + if (! isset($this->headers[$origName])) { + $this->setHeader($name, $value); + + return $this; + } + + if (! $this->hasMultipleHeaders($name) && isset($this->headers[$origName])) { + $this->headers[$origName] = [$this->headers[$origName]]; + } + + // Add the header. + $this->headers[$origName][] = new Header($origName, $value); + + return $this; + } + /** * Adds an additional header value to any headers that accept * multiple values (i.e. are an array or implement ArrayAccess) @@ -196,6 +254,8 @@ public function appendHeader(string $name, ?string $value): self */ public function prependHeader(string $name, string $value): self { + $this->checkMultipleHeaders($name); + $origName = $this->getHeaderName($name); $this->headers[$origName]->prependValue($value); diff --git a/system/HTTP/Method.php b/system/HTTP/Method.php new file mode 100644 index 000000000000..ee3a09ec4b0d --- /dev/null +++ b/system/HTTP/Method.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +/** + * HTTP Method List + */ +class Method +{ + /** + * Safe: No + * Idempotent: No + * Cacheable: No + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT + */ + public const CONNECT = 'CONNECT'; + + /** + * Safe: No + * Idempotent: Yes + * Cacheable: No + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE + */ + public const DELETE = 'DELETE'; + + /** + * Safe: Yes + * Idempotent: Yes + * Cacheable: Yes + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET + */ + public const GET = 'GET'; + + /** + * Safe: Yes + * Idempotent: Yes + * Cacheable: Yes + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD + */ + public const HEAD = 'HEAD'; + + /** + * Safe: Yes + * Idempotent: Yes + * Cacheable: No + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS + */ + public const OPTIONS = 'OPTIONS'; + + /** + * Safe: No + * Idempotent: No + * Cacheable: Only if freshness information is included + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH + */ + public const PATCH = 'PATCH'; + + /** + * Safe: No + * Idempotent: No + * Cacheable: Only if freshness information is included + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST + */ + public const POST = 'POST'; + + /** + * Safe: No + * Idempotent: Yes + * Cacheable: No + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT + */ + public const PUT = 'PUT'; + + /** + * Safe: Yes + * Idempotent: Yes + * Cacheable: No + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE + */ + public const TRACE = 'TRACE'; + + /** + * Returns all HTTP methods. + * + * @return list + */ + public static function all(): array + { + return [ + self::CONNECT, + self::DELETE, + self::GET, + self::HEAD, + self::OPTIONS, + self::PATCH, + self::POST, + self::PUT, + self::TRACE, + ]; + } +} diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index 61c3a58f4af7..67a03a38014d 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -1,5 +1,7 @@ method) : strtolower($this->method); + return $this->method; } /** diff --git a/system/HTTP/OutgoingRequestInterface.php b/system/HTTP/OutgoingRequestInterface.php index 3839b64fd8e1..da9e36206c37 100644 --- a/system/HTTP/OutgoingRequestInterface.php +++ b/system/HTTP/OutgoingRequestInterface.php @@ -1,5 +1,7 @@ reverseRoute($route, ...$params); + $route = service('routes')->reverseRoute($route, ...$params); if (! $route) { throw HTTPException::forInvalidRedirectRoute($namedRoute); @@ -75,7 +77,7 @@ public function route(string $route, array $params = [], ?int $code = null, stri */ public function back(?int $code = null, string $method = 'auto') { - Services::session(); + service('session'); return $this->redirect(previous_url(), $method, $code); } @@ -90,7 +92,7 @@ public function back(?int $code = null, string $method = 'auto') */ public function withInput() { - $session = Services::session(); + $session = service('session'); $session->setFlashdata('_ci_old_input', [ 'get' => $_GET ?? [], 'post' => $_POST ?? [], @@ -112,10 +114,10 @@ public function withInput() */ private function withErrors(): self { - $validation = Services::validation(); + $validation = service('validation'); if ($validation->getErrors()) { - $session = Services::session(); + $session = service('session'); $session->setFlashdata('_ci_validation_errors', $validation->getErrors()); } @@ -131,7 +133,7 @@ private function withErrors(): self */ public function with(string $key, $message) { - Services::session()->setFlashdata($key, $message); + service('session')->setFlashdata($key, $message); return $this; } @@ -161,8 +163,14 @@ public function withCookies() */ public function withHeaders() { - foreach (Services::response()->headers() as $name => $header) { - $this->setHeader($name, $header->getValue()); + foreach (Services::response()->headers() as $name => $value) { + if ($value instanceof Header) { + $this->setHeader($name, $value->getValue()); + } else { + foreach ($value as $header) { + $this->addHeader($name, $header->getValue()); + } + } } return $this; diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index afb0b22ad5d0..125646b6c7c7 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -1,5 +1,7 @@ - * - * @deprecated 4.0.5 No longer used. Check the App config directly - */ - protected $proxyIPs; - /** * Constructor. * * @param App $config - * - * @deprecated 4.0.5 The $config is no longer needed and will be removed in a future version */ - public function __construct($config = null) // @phpstan-ignore-line + public function __construct($config = null) { + $this->config = $config ?? config(App::class); + if (empty($this->method)) { - $this->method = $this->getServer('REQUEST_METHOD') ?? 'GET'; + $this->method = $this->getServer('REQUEST_METHOD') ?? Method::GET; } if (empty($this->uri)) { @@ -50,35 +42,6 @@ public function __construct($config = null) // @phpstan-ignore-line } } - /** - * Validate an IP address - * - * @param string $ip IP Address - * @param string $which IP protocol: 'ipv4' or 'ipv6' - * - * @deprecated 4.0.5 Use Validation instead - * - * @codeCoverageIgnore - */ - public function isValidIP(?string $ip = null, ?string $which = null): bool - { - return (new FormatRules())->valid_ip($ip, $which); - } - - /** - * Get the request method. - * - * @param bool $upper Whether to return in upper or lower case. - * - * @deprecated 4.0.5 The $upper functionality will be removed and this will revert to its PSR-7 equivalent - * - * @codeCoverageIgnore - */ - public function getMethod(bool $upper = false): string - { - return ($upper) ? strtoupper($this->method) : strtolower($this->method); - } - /** * Sets the request method. Used when spoofing the request. * diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php index 367c494928b8..26af51df9dab 100644 --- a/system/HTTP/RequestInterface.php +++ b/system/HTTP/RequestInterface.php @@ -1,5 +1,7 @@ proxyIPs; + $proxyIPs = $this->config->proxyIPs; if (! empty($proxyIPs) && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) { throw new ConfigException( @@ -77,7 +86,7 @@ public function getIPAddress(): string // @TODO Extract all this IP address logic to another class. foreach ($proxyIPs as $proxyIP => $header) { // Check if we have an IP address or a subnet - if (strpos($proxyIP, '/') === false) { + if (! str_contains($proxyIP, '/')) { // An IP address (and not a subnet) is specified. // We can compare right away. if ($proxyIP === $this->ipAddress) { @@ -98,7 +107,7 @@ public function getIPAddress(): string } // If the proxy entry doesn't match the IP protocol - skip it - if (strpos($proxyIP, $separator) === false) { + if (! str_contains($proxyIP, $separator)) { continue; } diff --git a/system/HTTP/ResponsableInterface.php b/system/HTTP/ResponsableInterface.php index 0cca5356f1f7..41bff03909e7 100644 --- a/system/HTTP/ResponsableInterface.php +++ b/system/HTTP/ResponsableInterface.php @@ -1,5 +1,7 @@ CSP = Services::csp(); - $this->CSPEnabled = $config->CSPEnabled; - $this->cookieStore = new CookieStore([]); $cookie = config(CookieConfig::class); diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 4455bc648ee0..827eea44e298 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -1,5 +1,7 @@ CSP->enabled() instead. - */ - protected $CSPEnabled = false; - /** * Content security policy handler * * @var ContentSecurityPolicy - * - * @deprecated Will be protected. Use `getCSP()` instead. */ - public $CSP; + protected $CSP; /** * CookieStore instance. @@ -59,69 +49,6 @@ trait ResponseTrait */ protected $cookieStore; - /** - * Set a cookie name prefix if you need to avoid collisions - * - * @var string - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookiePrefix = ''; - - /** - * Set to .your-domain.com for site-wide cookies - * - * @var string - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookieDomain = ''; - - /** - * Typically will be a forward slash - * - * @var string - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookiePath = '/'; - - /** - * Cookie will only be set if a secure HTTPS connection exists. - * - * @var bool - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookieSecure = false; - - /** - * Cookie will only be accessible via HTTP(S) (no javascript) - * - * @var bool - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookieHTTPOnly = false; - - /** - * Cookie SameSite setting - * - * @var string - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookieSameSite = Cookie::SAMESITE_LAX; - - /** - * Stores all cookies that were set in the response. - * - * @var array - * - * @deprecated Use the dedicated Cookie class instead. - */ - protected $cookies = []; - /** * Type of format the body is in. * Valid: html, json, xml @@ -262,7 +189,7 @@ public function getJSON() $body = $this->body; if ($this->bodyFormat !== 'json') { - $body = Services::format()->getFormatter('application/json')->format($body); + $body = service('format')->getFormatter('application/json')->format($body); } return $body ?: null; @@ -294,7 +221,7 @@ public function getXML() $body = $this->body; if ($this->bodyFormat !== 'xml') { - $body = Services::format()->getFormatter('application/xml')->format($body); + $body = service('format')->getFormatter('application/xml')->format($body); } return $body; @@ -319,7 +246,7 @@ protected function formatBody($body, string $format) // Nothing much to do for a string... if (! is_string($body) || $format === 'json-unencoded') { - $body = Services::format()->getFormatter($mime)->format($body); + $body = service('format')->getFormatter($mime)->format($body); } return $body; @@ -472,8 +399,22 @@ public function sendHeaders() header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode()); // Send all of our headers - foreach (array_keys($this->headers()) as $name) { - header($name . ': ' . $this->getHeaderLine($name), false, $this->getStatusCode()); + foreach ($this->headers() as $name => $value) { + if ($value instanceof Header) { + header( + $name . ': ' . $value->getValueLine(), + false, + $this->getStatusCode() + ); + } else { + foreach ($value as $header) { + header( + $name . ': ' . $header->getValueLine(), + false, + $this->getStatusCode() + ); + } + } } return $this; @@ -507,7 +448,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null if ( $method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) - && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false + && str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') ) { $method = 'refresh'; } elseif ($method !== 'refresh' && $code === null) { @@ -516,9 +457,9 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1 ) { - if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if ($_SERVER['REQUEST_METHOD'] === Method::GET) { $code = 302; - } elseif (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'DELETE'], true)) { + } elseif (in_array($_SERVER['REQUEST_METHOD'], [Method::POST, Method::PUT, Method::DELETE], true)) { // reference: https://en.wikipedia.org/wiki/Post/Redirect/Get $code = 303; } else { @@ -531,15 +472,10 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null $code = 302; } - switch ($method) { - case 'refresh': - $this->setHeader('Refresh', '0;url=' . $uri); - break; - - default: - $this->setHeader('Location', $uri); - break; - } + match ($method) { + 'refresh' => $this->setHeader('Refresh', '0;url=' . $uri), + default => $this->setHeader('Location', $uri), + }; $this->setStatusCode($code); @@ -554,7 +490,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null * * @param array|Cookie|string $name Cookie name / array containing binds / Cookie object * @param string $value Cookie value - * @param string $expire Cookie expiration time in seconds + * @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) @@ -567,7 +503,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null public function setCookie( $name, $value = '', - $expire = '', + $expire = 0, $domain = '', $path = '/', $prefix = '', @@ -581,14 +517,11 @@ public function setCookie( return $this; } - /** @var CookieConfig|null $cookieConfig */ $cookieConfig = config(CookieConfig::class); - if ($cookieConfig instanceof CookieConfig) { - $secure ??= $cookieConfig->secure; - $httponly ??= $cookieConfig->httponly; - $samesite ??= $cookieConfig->samesite; - } + $secure ??= $cookieConfig->secure; + $httponly ??= $cookieConfig->httponly; + $samesite ??= $cookieConfig->samesite; if (is_array($name)) { // always leave 'name' in last place, as the loop will break otherwise, due to ${$item} @@ -700,7 +633,7 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat } if (! $found) { - $this->setCookie($name, '', '', $domain, $path, $prefix); + $this->setCookie($name, '', 0, $domain, $path, $prefix); } return $this; @@ -733,7 +666,7 @@ protected function sendCookies() private function dispatchCookies(): void { /** @var IncomingRequest $request */ - $request = Services::request(); + $request = service('request'); foreach ($this->cookieStore->display() as $cookie) { if ($cookie->isSecure() && ! $request->isSecure()) { diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 8daa5ab2c0cd..def61b65e17b 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -1,5 +1,7 @@ appConfig = $appConfig; - $this->superglobals = $superglobals; } /** @@ -95,20 +92,11 @@ public function detectRoutePath(string $protocol = ''): string $protocol = $this->appConfig->uriProtocol; } - switch ($protocol) { - case 'REQUEST_URI': - $routePath = $this->parseRequestURI(); - break; - - case 'QUERY_STRING': - $routePath = $this->parseQueryString(); - break; - - case 'PATH_INFO': - default: - $routePath = $this->superglobals->server($protocol) ?? $this->parseRequestURI(); - break; - } + $routePath = match ($protocol) { + 'REQUEST_URI' => $this->parseRequestURI(), + 'QUERY_STRING' => $this->parseQueryString(), + default => $this->superglobals->server($protocol) ?? $this->parseRequestURI(), + }; return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/'); } @@ -161,7 +149,7 @@ private function parseRequestURI(): string // This section ensures that even on servers that require the URI to // contain the query string (Nginx) a correct URI is found, and also // fixes the QUERY_STRING Server var and $_GET array. - if (trim($path, '/') === '' && strncmp($query, '/', 1) === 0) { + if (trim($path, '/') === '' && str_starts_with($query, '/')) { $parts = explode('?', $query, 2); $path = $parts[0]; $newQuery = $query[1] ?? ''; @@ -193,7 +181,7 @@ private function parseQueryString(): string return '/'; } - if (strncmp($query, '/', 1) === 0) { + if (str_starts_with($query, '/')) { $parts = explode('?', $query, 2); $path = $parts[0]; $newQuery = $parts[1] ?? ''; diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 5bec38013bf2..efa99cf778a8 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -1,5 +1,7 @@ baseURL); if ( - substr($this->getScheme(), 0, 4) === 'http' + str_starts_with($this->getScheme(), 'http') && $this->getHost() === $baseUri->getHost() ) { // Check for additional segments $basePath = trim($baseUri->getPath(), '/') . '/'; $trimPath = ltrim($path, '/'); - if ($basePath !== '/' && strpos($trimPath, $basePath) !== 0) { + if ($basePath !== '/' && ! str_starts_with($trimPath, $basePath)) { $path = $basePath . $trimPath; } @@ -877,7 +880,7 @@ public function refreshPath() */ public function setQuery(string $query) { - if (strpos($query, '#') !== false) { + if (str_contains($query, '#')) { if ($this->silent) { return $this; } @@ -886,7 +889,7 @@ public function setQuery(string $query) } // Can't have leading ? - if ($query !== '' && strpos($query, '?') === 0) { + if ($query !== '' && str_starts_with($query, '?')) { $query = substr($query, 1); } @@ -1008,10 +1011,10 @@ protected function filterPath(?string $path = null): string $path = self::removeDotSegments($path); // Fix up some leading slash edge cases... - if (strpos($orig, './') === 0) { + if (str_starts_with($orig, './')) { $path = '/' . $path; } - if (strpos($orig, '../') === 0) { + if (str_starts_with($orig, '../')) { $path = '/' . $path; } @@ -1112,7 +1115,7 @@ public function resolveRelativeURI(string $uri) $transformed->setQuery($this->getQuery()); } } else { - if (strpos($relative->getPath(), '/') === 0) { + if (str_starts_with($relative->getPath(), '/')) { $transformed->setPath($relative->getPath()); } else { $transformed->setPath($this->mergePaths($this, $relative)); diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php index 2294b02a2b48..6a5104779a92 100644 --- a/system/HTTP/UserAgent.php +++ b/system/HTTP/UserAgent.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Helpers\Array; + +use InvalidArgumentException; + +/** + * @interal This is internal implementation for the framework. + * + * If there are any methods that should be provided, make them + * public APIs via helper functions. + * + * @see \CodeIgniter\Helpers\Array\ArrayHelperDotKeyExistsTest + * @see \CodeIgniter\Helpers\Array\ArrayHelperRecursiveDiffTest + * @see \CodeIgniter\Helpers\Array\ArrayHelperSortValuesByNaturalTest + */ +final class ArrayHelper +{ + /** + * Searches an array through dot syntax. Supports wildcard searches, + * like `foo.*.bar`. + * + * @used-by dot_array_search() + * + * @param string $index The index as dot array syntax. + * + * @return array|bool|int|object|string|null + */ + public static function dotSearch(string $index, array $array) + { + return self::arraySearchDot(self::convertToArray($index), $array); + } + + /** + * @param string $index The index as dot array syntax. + * + * @return list The index as an array. + */ + private static function convertToArray(string $index): array + { + // See https://regex101.com/r/44Ipql/1 + $segments = preg_split( + '/(? str_replace('\.', '.', $key), + $segments + ); + } + + /** + * Recursively search the array with wildcards. + * + * @used-by dotSearch() + * + * @return array|bool|float|int|object|string|null + */ + private static function arraySearchDot(array $indexes, array $array) + { + // If index is empty, returns null. + if ($indexes === []) { + return null; + } + + // Grab the current index + $currentIndex = array_shift($indexes); + + if (! isset($array[$currentIndex]) && $currentIndex !== '*') { + return null; + } + + // Handle Wildcard (*) + if ($currentIndex === '*') { + $answer = []; + + foreach ($array as $value) { + if (! is_array($value)) { + return null; + } + + $answer[] = self::arraySearchDot($indexes, $value); + } + + $answer = array_filter($answer, static fn ($value) => $value !== null); + + if ($answer !== []) { + // If array only has one element, we return that element for BC. + return count($answer) === 1 ? current($answer) : $answer; + } + + return null; + } + + // If this is the last index, make sure to return it now, + // and not try to recurse through things. + if ($indexes === []) { + return $array[$currentIndex]; + } + + // Do we need to recursively search this value? + if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) { + return self::arraySearchDot($indexes, $array[$currentIndex]); + } + + // Otherwise, not found. + return null; + } + + /** + * array_key_exists() with dot array syntax. + * + * If wildcard `*` is used, all items for the key after it must have the key. + */ + public static function dotKeyExists(string $index, array $array): bool + { + if (str_ends_with($index, '*') || str_contains($index, '*.*')) { + throw new InvalidArgumentException( + 'You must set key right after "*". Invalid index: "' . $index . '"' + ); + } + + $indexes = self::convertToArray($index); + + // If indexes is empty, returns false. + if ($indexes === []) { + return false; + } + + $currentArray = $array; + + // Grab the current index + while ($currentIndex = array_shift($indexes)) { + if ($currentIndex === '*') { + $currentIndex = array_shift($indexes); + + foreach ($currentArray as $item) { + if (! array_key_exists($currentIndex, $item)) { + return false; + } + } + + // If indexes is empty, all elements are checked. + if ($indexes === []) { + return true; + } + + $currentArray = self::dotSearch('*.' . $currentIndex, $currentArray); + + continue; + } + + if (! array_key_exists($currentIndex, $currentArray)) { + return false; + } + + $currentArray = $currentArray[$currentIndex]; + } + + return true; + } + + /** + * Groups all rows by their index values. Result's depth equals number of indexes + * + * @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 + * + * @return array Result array where rows are grouped together by indexes values. + */ + public static function groupBy(array $array, array $indexes, bool $includeEmpty = false): array + { + if ($indexes === []) { + return $array; + } + + $result = []; + + foreach ($array as $row) { + $result = self::arrayAttachIndexedValue($result, $row, $indexes, $includeEmpty); + } + + return $result; + } + + /** + * Recursively attach $row to the $indexes path of values found by + * `dot_array_search()`. + * + * @used-by groupBy() + */ + private static function arrayAttachIndexedValue( + array $result, + array $row, + array $indexes, + bool $includeEmpty + ): array { + if (($index = array_shift($indexes)) === null) { + $result[] = $row; + + return $result; + } + + $value = dot_array_search($index, $row); + + if (! is_scalar($value)) { + $value = ''; + } + + if (is_bool($value)) { + $value = (int) $value; + } + + if (! $includeEmpty && $value === '') { + return $result; + } + + if (! array_key_exists($value, $result)) { + $result[$value] = []; + } + + $result[$value] = self::arrayAttachIndexedValue($result[$value], $row, $indexes, $includeEmpty); + + return $result; + } + + /** + * Compare recursively two associative arrays and return difference as new array. + * Returns keys that exist in `$original` but not in `$compareWith`. + */ + public static function recursiveDiff(array $original, array $compareWith): array + { + $difference = []; + + if ($original === []) { + return []; + } + + if ($compareWith === []) { + return $original; + } + + foreach ($original as $originalKey => $originalValue) { + if ($originalValue === []) { + continue; + } + + if (is_array($originalValue)) { + $diffArrays = []; + + if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) { + $diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]); + } else { + $difference[$originalKey] = $originalValue; + } + + if ($diffArrays !== []) { + $difference[$originalKey] = $diffArrays; + } + } elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) { + $difference[$originalKey] = $originalValue; + } + } + + return $difference; + } + + /** + * Recursively count all keys. + */ + public static function recursiveCount(array $array, int $counter = 0): int + { + foreach ($array as $value) { + if (is_array($value)) { + $counter = self::recursiveCount($value, $counter); + } + + $counter++; + } + + return $counter; + } + + /** + * Sorts array values in natural order + * If the value is an array, you need to specify the $sortByIndex of the key to sort + * + * @param list|string> $array + * @param int|string|null $sortByIndex + */ + public static function sortValuesByNatural(array &$array, $sortByIndex = null): bool + { + return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex) { + if ($sortByIndex !== null) { + return strnatcmp((string) $currentValue[$sortByIndex], (string) $nextValue[$sortByIndex]); + } + + return strnatcmp((string) $currentValue, (string) $nextValue); + }); + } +} diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 4c1477f57d4a..837a612e4cef 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -1,5 +1,7 @@ str_replace('\.', '.', $key), $segments); - - return _array_search_dot($segments, $array); - } -} - -if (! function_exists('_array_search_dot')) { - /** - * Used by `dot_array_search` to recursively search the - * array with wildcards. - * - * @internal This should not be used on its own. - * - * @return array|bool|float|int|object|string|null - */ - function _array_search_dot(array $indexes, array $array) - { - // If index is empty, returns null. - if ($indexes === []) { - return null; - } - - // Grab the current index - $currentIndex = array_shift($indexes); - - if (! isset($array[$currentIndex]) && $currentIndex !== '*') { - return null; - } - - // Handle Wildcard (*) - if ($currentIndex === '*') { - $answer = []; - - foreach ($array as $value) { - if (! is_array($value)) { - return null; - } - - $answer[] = _array_search_dot($indexes, $value); - } - - $answer = array_filter($answer, static fn ($value) => $value !== null); - - if ($answer !== []) { - // If array only has one element, we return that element for BC. - return count($answer) === 1 ? current($answer) : $answer; - } - - return null; - } - - // If this is the last index, make sure to return it now, - // and not try to recurse through things. - if ($indexes === []) { - return $array[$currentIndex]; - } - - // Do we need to recursively search this value? - if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) { - return _array_search_dot($indexes, $array[$currentIndex]); - } - - // Otherwise, not found. - return null; + return ArrayHelper::dotSearch($index, $array); } } @@ -227,55 +160,6 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array */ function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array { - if ($indexes === []) { - return $array; - } - - $result = []; - - foreach ($array as $row) { - $result = _array_attach_indexed_value($result, $row, $indexes, $includeEmpty); - } - - return $result; - } -} - -if (! function_exists('_array_attach_indexed_value')) { - /** - * Used by `array_group_by` to recursively attach $row to the $indexes path of values found by - * `dot_array_search` - * - * @internal This should not be used on its own - */ - function _array_attach_indexed_value(array $result, array $row, array $indexes, bool $includeEmpty): array - { - if (($index = array_shift($indexes)) === null) { - $result[] = $row; - - return $result; - } - - $value = dot_array_search($index, $row); - - if (! is_scalar($value)) { - $value = ''; - } - - if (is_bool($value)) { - $value = (int) $value; - } - - if (! $includeEmpty && $value === '') { - return $result; - } - - if (! array_key_exists($value, $result)) { - $result[$value] = []; - } - - $result[$value] = _array_attach_indexed_value($result[$value], $row, $indexes, $includeEmpty); - - return $result; + return ArrayHelper::groupBy($array, $indexes, $includeEmpty); } } diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index de8fb6146a63..49e568346b68 100755 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -1,5 +1,7 @@ setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httpOnly, $sameSite); } } @@ -75,7 +76,7 @@ function get_cookie($index, bool $xssClean = false, ?string $prefix = '') $prefix = $cookie->prefix; } - $request = Services::request(); + $request = service('request'); $filter = $xssClean ? FILTER_SANITIZE_FULL_SPECIAL_CHARS : FILTER_DEFAULT; return $request->getCookie($prefix . $index, $filter); @@ -97,7 +98,7 @@ function get_cookie($index, bool $xssClean = false, ?string $prefix = '') */ function delete_cookie($name, string $domain = '', string $path = '/', string $prefix = '') { - Services::response()->deleteCookie($name, $domain, $path, $prefix); + service('response')->deleteCookie($name, $domain, $path, $prefix); } } @@ -107,6 +108,6 @@ function delete_cookie($name, string $domain = '', string $path = '/', string $p */ function has_cookie(string $name, ?string $value = null, string $prefix = ''): bool { - return Services::response()->hasCookie($name, $value, $prefix); + return service('response')->hasCookie($name, $value, $prefix); } } diff --git a/system/Helpers/date_helper.php b/system/Helpers/date_helper.php index cf1dc33a74ea..c0b4d5ebe17a 100644 --- a/system/Helpers/date_helper.php +++ b/system/Helpers/date_helper.php @@ -1,5 +1,7 @@ getLocale(), $action); + if (str_contains($action, '{locale}')) { + $action = str_replace('{locale}', service('request')->getLocale(), $action); } $action = site_url($action); @@ -59,9 +60,9 @@ function form_open(string $action = '', $attributes = [], array $hidden = []): s $form = '
\n"; // Add CSRF field if enabled, but leave it out for GET requests and requests to external websites - $before = Services::filters()->getFilters()['before']; + $before = service('filters')->getFilters()['before']; - if ((in_array('csrf', $before, true) || array_key_exists('csrf', $before)) && strpos($action, base_url()) !== false && ! stripos($form, 'method="get"')) { + if ((in_array('csrf', $before, true) || array_key_exists('csrf', $before)) && str_contains($action, base_url()) && ! stripos($form, 'method="get"')) { $form .= csrf_field($csrfId ?? null); } @@ -550,7 +551,7 @@ function form_close(string $extra = ''): string */ function set_value(string $field, $default = '', bool $htmlEscape = true) { - $request = Services::request(); + $request = service('request'); // Try any old input data we may have first $value = $request->getOldInput($field); @@ -571,7 +572,7 @@ function set_value(string $field, $default = '', bool $htmlEscape = true) */ function set_select(string $field, string $value = '', bool $default = false): string { - $request = Services::request(); + $request = service('request'); // Try any old input data we may have first $input = $request->getOldInput($field); @@ -607,7 +608,7 @@ function set_select(string $field, string $value = '', bool $default = false): s */ function set_checkbox(string $field, string $value = '', bool $default = false): string { - $request = Services::request(); + $request = service('request'); // Try any old input data we may have first $input = $request->getOldInput($field); @@ -627,7 +628,7 @@ function set_checkbox(string $field, string $value = '', bool $default = false): return ''; } - $session = Services::session(); + $session = service('session'); $hasOldInput = $session->has('_ci_old_input'); // Unchecked checkbox and radio inputs are not even submitted by browsers ... @@ -647,7 +648,7 @@ function set_checkbox(string $field, string $value = '', bool $default = false): */ function set_radio(string $field, string $value = '', bool $default = false): string { - $request = Services::request(); + $request = service('request'); // Try any old input data we may have first $oldInput = $request->getOldInput($field); @@ -701,7 +702,7 @@ function validation_errors() return $errors; } - $validation = Services::validation(); + $validation = service('validation'); return $validation->getErrors(); } @@ -716,7 +717,7 @@ function validation_errors() function validation_list_errors(string $template = 'list'): string { $config = config(Validation::class); - $view = Services::renderer(); + $view = service('renderer'); if (! array_key_exists($template, $config->templates)) { throw ValidationException::forInvalidTemplate($template); @@ -736,7 +737,7 @@ function validation_list_errors(string $template = 'list'): string function validation_show_error(string $field, string $template = 'single'): string { $config = config(Validation::class); - $view = Services::renderer(); + $view = service('renderer'); $errors = array_filter(validation_errors(), static fn ($key) => preg_match( '/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/', diff --git a/system/Helpers/html_helper.php b/system/Helpers/html_helper.php index 6d9a51356af6..f3d33611dcef 100755 --- a/system/Helpers/html_helper.php +++ b/system/Helpers/html_helper.php @@ -1,5 +1,7 @@ list; - return $doctypes[$type] ?? false; + return $doctypes[$type] ?? ''; } } diff --git a/system/Helpers/inflector_helper.php b/system/Helpers/inflector_helper.php index bac5ab1e2d6f..abc565652ecb 100755 --- a/system/Helpers/inflector_helper.php +++ b/system/Helpers/inflector_helper.php @@ -1,5 +1,7 @@ setAttribute(NumberFormatter::FRACTION_DIGITS, $options['fraction']); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, (float) $options['fraction']); $output = $formatter->formatCurrency($num, $options['currency']); } else { // In order to specify a precision, we'll have to modify diff --git a/system/Helpers/security_helper.php b/system/Helpers/security_helper.php index eba12de59ad8..af6257e5e39c 100644 --- a/system/Helpers/security_helper.php +++ b/system/Helpers/security_helper.php @@ -1,5 +1,7 @@ sanitizeFilename($filename); + return service('security')->sanitizeFilename($filename); } } diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index a542bb1ca73c..f86ca468b0cc 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -1,5 +1,7 @@ getUri(); + $currentURI = service('request')->getUri(); assert($currentURI instanceof SiteURI); @@ -51,7 +52,7 @@ function site_url($relativePath = '', ?string $scheme = null, ?App $config = nul */ function base_url($relativePath = '', ?string $scheme = null): string { - $currentURI = Services::request()->getUri(); + $currentURI = service('request')->getUri(); assert($currentURI instanceof SiteURI); @@ -71,7 +72,7 @@ function base_url($relativePath = '', ?string $scheme = null): string */ function current_url(bool $returnObject = false, ?IncomingRequest $request = null) { - $request ??= Services::request(); + $request ??= service('request'); /** @var CLIRequest|IncomingRequest $request */ $uri = $request->getUri(); @@ -111,9 +112,9 @@ function previous_url(bool $returnObject = false) */ function uri_string(): string { - // The value of Services::request()->getUri()->getPath() returns + // The value of service('request')->getUri()->getPath() returns // full URI path. - $uri = Services::request()->getUri(); + $uri = service('request')->getUri(); $path = $uri instanceof SiteURI ? $uri->getRoutePath() : $uri->getPath(); diff --git a/system/Helpers/xml_helper.php b/system/Helpers/xml_helper.php index ece282013721..13f9c0dfa55d 100644 --- a/system/Helpers/xml_helper.php +++ b/system/Helpers/xml_helper.php @@ -1,5 +1,7 @@ config = $config; - if (! $this->config->hidden) { - throw HoneypotException::forNoHiddenValue(); - } - - if ($this->config->container === '' || strpos($this->config->container, '{template}') === false) { + if ($this->config->container === '' || ! str_contains($this->config->container, '{template}')) { $this->config->container = '
{template}
'; } diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index ba8d2bfbb891..0910f2fa94bc 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -1,5 +1,7 @@ 12)) { - throw I18nException::forInvalidMonth($value); + throw I18nException::forInvalidMonth((string) $value); } if (is_string($value) && ! is_numeric($value)) { @@ -574,13 +576,13 @@ public function setMonth($value) public function setDay($value) { if ($value < 1 || $value > 31) { - throw I18nException::forInvalidDay($value); + throw I18nException::forInvalidDay((string) $value); } $date = $this->getYear() . '-' . $this->getMonth(); $lastDay = date('t', strtotime($date)); if ($value > $lastDay) { - throw I18nException::forInvalidOverDay($lastDay, $value); + throw I18nException::forInvalidOverDay($lastDay, (string) $value); } return $this->setValue('day', $value); @@ -598,7 +600,7 @@ public function setDay($value) public function setHour($value) { if ($value < 0 || $value > 23) { - throw I18nException::forInvalidHour($value); + throw I18nException::forInvalidHour((string) $value); } return $this->setValue('hour', $value); @@ -616,7 +618,7 @@ public function setHour($value) public function setMinute($value) { if ($value < 0 || $value > 59) { - throw I18nException::forInvalidMinutes($value); + throw I18nException::forInvalidMinutes((string) $value); } return $this->setValue('minute', $value); @@ -634,7 +636,7 @@ public function setMinute($value) public function setSecond($value) { if ($value < 0 || $value > 59) { - throw I18nException::forInvalidSeconds($value); + throw I18nException::forInvalidSeconds((string) $value); } return $this->setValue('second', $value); @@ -1017,7 +1019,7 @@ public function isAfter($testTime, ?string $timezone = null): bool */ public function humanize() { - $now = IntlCalendar::fromDateTime(self::now($this->timezone)); + $now = IntlCalendar::fromDateTime(self::now($this->timezone)->toDateTime()); $time = $this->getCalendar()->getTime(); $years = $now->fieldDifference($time, IntlCalendar::FIELD_YEAR); @@ -1131,7 +1133,7 @@ public function getUTCObject($time, ?string $timezone = null) */ public function getCalendar() { - return IntlCalendar::fromDateTime($this); + return IntlCalendar::fromDateTime($this->toDateTime()); } /** diff --git a/system/Images/Exceptions/ImageException.php b/system/Images/Exceptions/ImageException.php index e72a19f8b6d3..93491bfa9e1b 100644 --- a/system/Images/Exceptions/ImageException.php +++ b/system/Images/Exceptions/ImageException.php @@ -1,5 +1,7 @@ getEXIF('Orientation', $silent); - switch ($orientation) { - case 2: - return $this->flip('horizontal'); - - case 3: - return $this->rotate(180); - - case 4: - return $this->rotate(180)->flip('horizontal'); - - case 5: - return $this->rotate(270)->flip('horizontal'); - - case 6: - return $this->rotate(270); - - case 7: - return $this->rotate(90)->flip('horizontal'); - - case 8: - return $this->rotate(90); - - default: - return $this; - } + return match ($orientation) { + 2 => $this->flip('horizontal'), + 3 => $this->rotate(180), + 4 => $this->rotate(180)->flip('horizontal'), + 5 => $this->rotate(270)->flip('horizontal'), + 6 => $this->rotate(270), + 7 => $this->rotate(90)->flip('horizontal'), + 8 => $this->rotate(90), + default => $this, + }; } /** @@ -559,12 +546,12 @@ public function fit(int $width, ?int $height = null, string $position = 'center' [$cropWidth, $cropHeight] = $this->calcAspectRatio($width, $height, $origWidth, $origHeight); if ($height === null) { - $height = ceil(($width / $cropWidth) * $cropHeight); + $height = (int) ceil(($width / $cropWidth) * $cropHeight); } [$x, $y] = $this->calcCropCoords($cropWidth, $cropHeight, $origWidth, $origHeight, $position); - return $this->crop($cropWidth, $cropHeight, $x, $y)->resize($width, $height); + return $this->crop($cropWidth, $cropHeight, (int) $x, (int) $y)->resize($width, $height); } /** diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index f2cd55874044..350dbc2306a8 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -1,5 +1,7 @@ getEXIF('Orientation', $silent); - switch ($orientation) { - case 2: - return $this->flip('horizontal'); - - case 3: - return $this->rotate(180); - - case 4: - return $this->rotate(180)->flip('horizontal'); - - case 5: - return $this->rotate(90)->flip('horizontal'); - - case 6: - return $this->rotate(90); - - case 7: - return $this->rotate(270)->flip('horizontal'); - - case 8: - return $this->rotate(270); - - default: - return $this; - } + return match ($orientation) { + 2 => $this->flip('horizontal'), + 3 => $this->rotate(180), + 4 => $this->rotate(180)->flip('horizontal'), + 5 => $this->rotate(90)->flip('horizontal'), + 6 => $this->rotate(90), + 7 => $this->rotate(270)->flip('horizontal'), + 8 => $this->rotate(270), + default => $this, + }; } } diff --git a/system/Images/Image.php b/system/Images/Image.php index 291ad002a6f2..a2cf51436121 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -1,5 +1,7 @@ formatMessage($line, $args); } @@ -272,7 +273,7 @@ protected function load(string $file, string $locale, bool $return = false) */ protected function requireFile(string $path): array { - $files = Services::locator()->search($path, 'php', false); + $files = service('locator')->search($path, 'php', false); $strings = []; foreach ($files as $file) { diff --git a/system/Language/en/CLI.php b/system/Language/en/CLI.php index edef33a7d1f2..f636fb2131d4 100644 --- a/system/Language/en/CLI.php +++ b/system/Language/en/CLI.php @@ -1,5 +1,7 @@ 'Migration class name', 'model' => 'Model class name', 'seeder' => 'Seeder class name', + 'test' => 'Test class name', 'validation' => 'Validation class name', ], 'commandType' => 'Command type', diff --git a/system/Language/en/Cache.php b/system/Language/en/Cache.php index d2abc9bedbbc..b877c9bbaabd 100644 --- a/system/Language/en/Cache.php +++ b/system/Language/en/Cache.php @@ -1,5 +1,7 @@ 'The {field} field must differ from the {param} field.', 'equals' => 'The {field} field must be exactly: {param}.', 'exact_length' => 'The {field} field must be exactly {param} characters in length.', + 'field_exists' => 'The {field} field must exist.', 'greater_than' => 'The {field} field must contain a number greater than {param}.', 'greater_than_equal_to' => 'The {field} field must contain a number greater than or equal to {param}.', 'hex' => 'The {field} field may only contain hexadecimal characters.', diff --git a/system/Language/en/View.php b/system/Language/en/View.php index cd6b6c294ade..79d57f06b92e 100644 --- a/system/Language/en/View.php +++ b/system/Language/en/View.php @@ -1,5 +1,7 @@ dateFormat, 'u') !== false) { + if (str_contains($this->dateFormat, 'u')) { $microtimeFull = microtime(true); $microtimeShort = sprintf('%06d', ($microtimeFull - floor($microtimeFull)) * 1_000_000); $date = new DateTime(date('Y-m-d H:i:s.' . $microtimeShort, (int) $microtimeFull)); diff --git a/system/Log/Handlers/HandlerInterface.php b/system/Log/Handlers/HandlerInterface.php index 7b809a320d65..40a9958c714a 100644 --- a/system/Log/Handlers/HandlerInterface.php +++ b/system/Log/Handlers/HandlerInterface.php @@ -1,5 +1,7 @@ log('emergency', $message, $context); + $this->log('emergency', $message, $context); } /** @@ -169,9 +172,9 @@ public function emergency($message, array $context = []): bool * * @param string $message */ - public function alert($message, array $context = []): bool + public function alert(string|Stringable $message, array $context = []): void { - return $this->log('alert', $message, $context); + $this->log('alert', $message, $context); } /** @@ -181,9 +184,9 @@ public function alert($message, array $context = []): bool * * @param string $message */ - public function critical($message, array $context = []): bool + public function critical(string|Stringable $message, array $context = []): void { - return $this->log('critical', $message, $context); + $this->log('critical', $message, $context); } /** @@ -192,9 +195,9 @@ public function critical($message, array $context = []): bool * * @param string $message */ - public function error($message, array $context = []): bool + public function error(string|Stringable $message, array $context = []): void { - return $this->log('error', $message, $context); + $this->log('error', $message, $context); } /** @@ -205,9 +208,9 @@ public function error($message, array $context = []): bool * * @param string $message */ - public function warning($message, array $context = []): bool + public function warning(string|Stringable $message, array $context = []): void { - return $this->log('warning', $message, $context); + $this->log('warning', $message, $context); } /** @@ -215,9 +218,9 @@ public function warning($message, array $context = []): bool * * @param string $message */ - public function notice($message, array $context = []): bool + public function notice(string|Stringable $message, array $context = []): void { - return $this->log('notice', $message, $context); + $this->log('notice', $message, $context); } /** @@ -227,9 +230,9 @@ public function notice($message, array $context = []): bool * * @param string $message */ - public function info($message, array $context = []): bool + public function info(string|Stringable $message, array $context = []): void { - return $this->log('info', $message, $context); + $this->log('info', $message, $context); } /** @@ -237,9 +240,9 @@ public function info($message, array $context = []): bool * * @param string $message */ - public function debug($message, array $context = []): bool + public function debug(string|Stringable $message, array $context = []): void { - return $this->log('debug', $message, $context); + $this->log('debug', $message, $context); } /** @@ -248,7 +251,7 @@ public function debug($message, array $context = []): bool * @param string $level * @param string $message */ - public function log($level, $message, array $context = []): bool + public function log($level, string|Stringable $message, array $context = []): void { if (is_numeric($level)) { $level = array_search((int) $level, $this->logLevels, true); @@ -261,7 +264,7 @@ public function log($level, $message, array $context = []): bool // Does the app want to log this right now? if (! in_array($level, $this->loggableLevels, true)) { - return false; + return; } // Parse our placeholders @@ -294,8 +297,6 @@ public function log($level, $message, array $context = []): bool break; } } - - return true; } /** @@ -340,7 +341,7 @@ protected function interpolate($message, array $context = []) $replace['{env}'] = ENVIRONMENT; // Allow us to log the file/line that we are logging from - if (strpos($message, '{file}') !== false) { + if (str_contains($message, '{file}')) { [$file, $line] = $this->determineFile(); $replace['{file}'] = $file; @@ -348,7 +349,7 @@ protected function interpolate($message, array $context = []) } // Match up environment variables in {env:foo} tags. - if (strpos($message, 'env:') !== false) { + if (str_contains($message, 'env:')) { preg_match('/env:[^}]+/', $message, $matches); foreach ($matches as $str) { diff --git a/system/Model.php b/system/Model.php index db912d7166d7..61f4350f3117 100644 --- a/system/Model.php +++ b/system/Model.php @@ -1,5 +1,7 @@ builder(); + $useCast = $this->useCasts(); + if ($useCast) { + $returnType = $this->tempReturnType; + $this->asArray(); + } + if ($this->tempUseSoftDeletes) { $builder->where($this->table . '.' . $this->deletedField, null); } @@ -203,6 +209,12 @@ protected function doFind(bool $singleton, $id = null) $row = $builder->get()->getResult($this->tempReturnType); } + if ($useCast) { + $row = $this->convertToReturnType($row, $returnType); + + $this->tempReturnType = $returnType; + } + return $row; } @@ -217,7 +229,15 @@ protected function doFind(bool $singleton, $id = null) */ protected function doFindColumn(string $columnName) { - return $this->select($columnName)->asArray()->find(); + $results = $this->select($columnName)->asArray()->find(); + + if ($this->useCasts()) { + foreach ($results as $i => $row) { + $results[$i] = $this->converter->fromDataSource($row); + } + } + + return $results; } /** @@ -225,23 +245,44 @@ protected function doFindColumn(string $columnName) * all results, while optionally limiting them. * This method works only with dbCalls. * - * @param int $limit Limit - * @param int $offset Offset + * @param int|null $limit Limit + * @param int $offset Offset * * @return array * @phpstan-return list */ - protected function doFindAll(int $limit = 0, int $offset = 0) + protected function doFindAll(?int $limit = null, int $offset = 0) { + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + if ($limitZeroAsAll) { + $limit ??= 0; + } + $builder = $this->builder(); + $useCast = $this->useCasts(); + if ($useCast) { + $returnType = $this->tempReturnType; + $this->asArray(); + } + if ($this->tempUseSoftDeletes) { $builder->where($this->table . '.' . $this->deletedField, null); } - return $builder->limit($limit, $offset) + $results = $builder->limit($limit, $offset) ->get() ->getResult($this->tempReturnType); + + if ($useCast) { + foreach ($results as $i => $row) { + $results[$i] = $this->convertToReturnType($row, $returnType); + } + + $this->tempReturnType = $returnType; + } + + return $results; } /** @@ -256,6 +297,12 @@ protected function doFirst() { $builder = $this->builder(); + $useCast = $this->useCasts(); + if ($useCast) { + $returnType = $this->tempReturnType; + $this->asArray(); + } + if ($this->tempUseSoftDeletes) { $builder->where($this->table . '.' . $this->deletedField, null); } elseif ($this->useSoftDeletes && ($builder->QBGroupBy === []) && $this->primaryKey) { @@ -268,7 +315,15 @@ protected function doFirst() $builder->orderBy($this->table . '.' . $this->primaryKey, 'asc'); } - return $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType); + $row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType); + + if ($useCast) { + $row = $this->convertToReturnType($row, $returnType); + + $this->tempReturnType = $returnType; + } + + return $row; } /** @@ -512,21 +567,7 @@ protected function doErrors() return []; } - return [get_class($this->db) => $error['message']]; - } - - /** - * Returns the id value for the data array or object - * - * @param array|object $data Data - * - * @return array|int|string|null - * - * @deprecated Use getIdValue() instead. Will be removed in version 5.0. - */ - protected function idValue($data) - { - return $this->getIdValue($data); + return [$this->db::class => $error['message']]; } /** @@ -819,7 +860,7 @@ public function update($id = null, $row = null): bool * @param object $object Object * @param bool $recursive If true, inner entities will be cast as array as well * - * @return array + * @return array Array with raw values. * * @throws ReflectionException */ @@ -895,70 +936,4 @@ private function checkBuilderMethod(string $name): void throw ModelException::forMethodNotAvailable(static::class, $name . '()'); } } - - /** - * Takes a class an returns an array of it's public and protected - * properties as an array suitable for use in creates and updates. - * - * @param object|string $data - * @param string|null $primaryKey - * - * @throws ReflectionException - * - * @codeCoverageIgnore - * - * @deprecated 4.1.0 - */ - public static function classToArray($data, $primaryKey = null, string $dateFormat = 'datetime', bool $onlyChanged = true): array - { - if (method_exists($data, 'toRawArray')) { - $properties = $data->toRawArray($onlyChanged); - - // Always grab the primary key otherwise updates will fail. - if ($properties !== [] && isset($primaryKey) && ! in_array($primaryKey, $properties, true) && isset($data->{$primaryKey})) { - $properties[$primaryKey] = $data->{$primaryKey}; - } - } else { - $mirror = new ReflectionClass($data); - $props = $mirror->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED); - - $properties = []; - - // Loop over each property, - // saving the name/value in a new array we can return. - foreach ($props as $prop) { - // Must make protected values accessible. - $prop->setAccessible(true); - $properties[$prop->getName()] = $prop->getValue($data); - } - } - - // Convert any Time instances to appropriate $dateFormat - if ($properties) { - foreach ($properties as $key => $value) { - if ($value instanceof Time) { - switch ($dateFormat) { - case 'datetime': - $converted = $value->format('Y-m-d H:i:s'); - break; - - case 'date': - $converted = $value->format('Y-m-d'); - break; - - case 'int': - $converted = $value->getTimestamp(); - break; - - default: - $converted = (string) $value; - } - - $properties[$key] = $converted; - } - } - } - - return $properties; - } } diff --git a/system/Modules/Modules.php b/system/Modules/Modules.php index e17b9f3c2415..ca4fe40ab128 100644 --- a/system/Modules/Modules.php +++ b/system/Modules/Modules.php @@ -1,5 +1,7 @@ groups[$group]['currentPage'] = (int) $this->groups[$group]['currentUri'] ->setSilent(false)->getSegment($this->segment[$group]); - } catch (HTTPException $e) { + } catch (HTTPException) { $this->groups[$group]['currentPage'] = 1; } } else { diff --git a/system/Pager/PagerInterface.php b/system/Pager/PagerInterface.php index 84018d7ac5f9..650b04f48e30 100644 --- a/system/Pager/PagerInterface.php +++ b/system/Pager/PagerInterface.php @@ -1,5 +1,7 @@ */ - private array $restrictions; + private readonly array $restrictions; - private ContentReplacer $replacer; + private readonly ContentReplacer $replacer; /** * Base path to use for the source. @@ -105,7 +107,7 @@ final public static function discover(string $directory = 'Publishers'): array self::$discovered[$directory] = []; - /** @var FileLocator $locator */ + /** @var FileLocatorInterface $locator */ $locator = service('locator'); if ([] === $files = $locator->listFiles($directory)) { @@ -114,9 +116,9 @@ final public static function discover(string $directory = 'Publishers'): array // Loop over each file checking to see if it is a Publisher foreach (array_unique($files) as $file) { - $className = $locator->getClassname($file); + $className = $locator->findQualifiedNameFromPath($file); - if ($className !== '' && class_exists($className) && is_a($className, self::class, true)) { + if ($className !== false && class_exists($className) && is_a($className, self::class, true)) { self::$discovered[$directory][] = new $className(); } } @@ -167,7 +169,7 @@ public function __construct(?string $source = null, ?string $destination = null) // Make sure the destination is allowed foreach (array_keys($this->restrictions) as $directory) { - if (strpos($this->destination, $directory) === 0) { + if (str_starts_with($this->destination, $directory)) { return; } } @@ -470,7 +472,7 @@ private function verifyAllowed(string $from, string $to): void { // Verify this is an allowed file for its destination foreach ($this->restrictions as $directory => $pattern) { - if (strpos($to, $directory) === 0 && self::matchFiles([$to], $pattern) === []) { + if (str_starts_with($to, $directory) && self::matchFiles([$to], $pattern) === []) { throw PublisherException::forFileNotAllowed($from, $directory, $pattern); } } diff --git a/system/RESTful/BaseResource.php b/system/RESTful/BaseResource.php index 1c21f65da860..0360bdb12fc5 100644 --- a/system/RESTful/BaseResource.php +++ b/system/RESTful/BaseResource.php @@ -1,5 +1,7 @@ model) && empty($this->modelName)) { - $this->modelName = get_class($this->model); + $this->modelName = $this->model::class; } } } diff --git a/system/RESTful/ResourceController.php b/system/RESTful/ResourceController.php index bd0356268142..072b52ee8260 100644 --- a/system/RESTful/ResourceController.php +++ b/system/RESTful/ResourceController.php @@ -1,5 +1,7 @@ [routeKey => handler] - */ - private array $cliRoutes; - /** * Sub-directory that contains the requested controller class. * Primarily used by 'autoRoute'. */ private ?string $directory = null; - /** - * The name of the controller class. - */ - private string $controller; - - /** - * The name of the method to use. - */ - private string $method; - - /** - * Whether dashes in URI's should be converted - * to underscores when determining method names. - */ - private bool $translateURIDashes; - - /** - * HTTP verb for the request. - */ - private string $httpVerb; - - /** - * Default namespace for controllers. - */ - private string $defaultNamespace; - public function __construct( - array $cliRoutes, - string $defaultNamespace, - string $defaultController, - string $defaultMethod, - bool $translateURIDashes, - string $httpVerb + /** + * List of CLI routes that do not contain '*' routes. + * + * @var array [routeKey => handler] + */ + private readonly array $cliRoutes, + /** + * Default namespace for controllers. + */ + private readonly string $defaultNamespace, + /** + * The name of the controller class. + */ + private string $controller, + /** + * The name of the method to use. + */ + private string $method, + /** + * Whether dashes in URI's should be converted + * to underscores when determining method names. + */ + private bool $translateURIDashes ) { - $this->cliRoutes = $cliRoutes; - $this->defaultNamespace = $defaultNamespace; - $this->translateURIDashes = $translateURIDashes; - $this->httpVerb = $httpVerb; - - $this->controller = $defaultController; - $this->method = $defaultMethod; } /** * 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 @@ -121,7 +102,7 @@ public function getRoute(string $uri, string $httpVerb): array } // Ensure routes registered via $routes->cli() are not accessible via web. - if ($this->httpVerb !== 'cli') { + if ($httpVerb !== 'CLI') { $controller = '\\' . $this->defaultNamespace; $controller .= $this->directory ? str_replace('/', '\\', $this->directory) : ''; @@ -135,13 +116,13 @@ public function getRoute(string $uri, string $httpVerb): array $handler = strtolower($handler); // Like $routes->cli('hello/(:segment)', 'Home::$1') - if (strpos($handler, '::$') !== false) { + if (str_contains($handler, '::$')) { throw new PageNotFoundException( 'Cannot access CLI Route: ' . $uri ); } - if (strpos($handler, $controller . '::' . $methodName) === 0) { + if (str_starts_with($handler, $controller . '::' . $methodName)) { throw new PageNotFoundException( 'Cannot access CLI Route: ' . $uri ); @@ -158,13 +139,16 @@ public function getRoute(string $uri, string $httpVerb): array // Load the file so that it's available for CodeIgniter. $file = APPPATH . 'Controllers/' . $this->directory . $controllerName . '.php'; - if (is_file($file)) { - include_once $file; + + if (! is_file($file)) { + throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } + include_once $file; + // Ensure the controller stores the fully-qualified class name // We have to check for a length over 1, since by default it will be '\' - if (strpos($this->controller, '\\') === false && strlen($this->defaultNamespace) > 1) { + if (! str_contains($this->controller, '\\') && strlen($this->defaultNamespace) > 1) { $this->controller = '\\' . ltrim( str_replace( '/', diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index a21814fca47e..0db5ebc1ddc2 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -1,5 +1,7 @@ - */ - private array $protectedControllers; - /** * Sub-directory that contains the requested controller class. */ @@ -54,10 +49,10 @@ final class AutoRouterImproved implements AutoRouterInterface private array $params = []; /** - * Whether dashes in URI's should be converted - * to underscores when determining method names. + * Whether to translate dashes in URIs for controller/method to CamelCase. + * E.g., blog-controller -> BlogController */ - private bool $translateURIDashes; + private readonly bool $translateUriToCamelCase; /** * The namespace for controllers. @@ -65,14 +60,17 @@ final class AutoRouterImproved implements AutoRouterInterface private string $namespace; /** - * The name of the default controller class. - */ - private string $defaultController; - - /** - * The name of the default method without HTTP verb prefix. + * 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', + * ] + * + * @var array [ uri_segment => namespace ] */ - private string $defaultMethod; + private array $moduleRoutes; /** * The URI segments. @@ -99,25 +97,37 @@ final class AutoRouterImproved implements AutoRouterInterface */ private ?int $paramPos = null; + /** + * The current URI + */ + private ?string $uri = null; + /** * @param list $protectedControllers * @param string $defaultController Short classname - * - * @deprecated $httpVerb is deprecated. No longer used. */ - public function __construct(// @phpstan-ignore-line - array $protectedControllers, + public function __construct( + /** + * List of controllers in Defined Routes that should not be accessed via this Auto-Routing. + */ + private readonly array $protectedControllers, string $namespace, - string $defaultController, - string $defaultMethod, - bool $translateURIDashes, - string $httpVerb + private readonly string $defaultController, + /** + * The name of the default method without HTTP verb prefix. + */ + private readonly string $defaultMethod, + /** + * Whether dashes in URI's should be converted + * to underscores when determining method names. + */ + private readonly bool $translateURIDashes ) { - $this->protectedControllers = $protectedControllers; - $this->namespace = rtrim($namespace, '\\'); - $this->translateURIDashes = $translateURIDashes; - $this->defaultController = $defaultController; - $this->defaultMethod = $defaultMethod; + $this->namespace = rtrim($namespace, '\\'); + + $routingConfig = config(Routing::class); + $this->moduleRoutes = $routingConfig->moduleRoutes; + $this->translateUriToCamelCase = $routingConfig->translateUriToCamelCase; // Set the default values $this->controller = $this->defaultController; @@ -152,7 +162,7 @@ private function searchFirstController(): bool $segment = array_shift($segments); $controllerPos++; - $class = $this->translateURIDashes(ucfirst($segment)); + $class = $this->translateURIDashes($segment); // as soon as we encounter any segment that is not PSR-4 compliant, stop searching if (! $this->isValidSegment($class)) { @@ -165,6 +175,8 @@ private function searchFirstController(): bool $this->controller = $controller; $this->controllerPos = $controllerPos; + $this->checkUriForController($controller); + // The first item may be a method name. $this->params = $segments; if ($segments !== []) { @@ -197,7 +209,7 @@ private function searchLastDefaultController(): bool } $namespaces = array_map( - fn ($segment) => $this->translateURIDashes(ucfirst($segment)), + fn ($segment) => $this->translateURIDashes($segment), $segments ); @@ -241,11 +253,14 @@ private function searchLastDefaultController(): bool /** * 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 { - $httpVerb = strtolower($httpVerb); + $this->uri = $uri; + $httpVerb = strtolower($httpVerb); // Reset Controller method params. $this->params = []; @@ -258,11 +273,10 @@ public function getRoute(string $uri, string $httpVerb): array // Check for Module Routes. if ( $this->segments !== [] - && ($routingConfig = config(Routing::class)) - && array_key_exists($this->segments[0], $routingConfig->moduleRoutes) + && array_key_exists($this->segments[0], $this->moduleRoutes) ) { $uriSegment = array_shift($this->segments); - $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\'); + $this->namespace = rtrim($this->moduleRoutes[$uriSegment], '\\'); } if ($this->searchFirstController()) { @@ -293,7 +307,9 @@ public function getRoute(string $uri, string $httpVerb): array $method = ''; if ($methodParam !== null) { - $method = $httpVerb . ucfirst($this->translateURIDashes($methodParam)); + $method = $httpVerb . $this->translateURIDashes($methodParam); + + $this->checkUriForMethod($method); } if ($methodParam !== null && method_exists($this->controller, $method)) { @@ -339,12 +355,12 @@ public function getRoute(string $uri, string $httpVerb): array // Ensure the URI segments for the controller and method do not contain // underscores when $translateURIDashes is true. - $this->checkUnderscore($uri); + $this->checkUnderscore(); // Check parameter count try { - $this->checkParameters($uri); - } catch (MethodNotFoundException $e) { + $this->checkParameters(); + } catch (MethodNotFoundException) { throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } @@ -407,18 +423,18 @@ private function protectDefinedRoutes(): void } } - private function checkParameters(string $uri): void + private function checkParameters(): void { try { $refClass = new ReflectionClass($this->controller); - } catch (ReflectionException $e) { + } catch (ReflectionException) { throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } try { $refMethod = $refClass->getMethod($this->method); $refParams = $refMethod->getParameters(); - } catch (ReflectionException $e) { + } catch (ReflectionException) { throw new MethodNotFoundException(); } @@ -430,7 +446,7 @@ private function checkParameters(string $uri): void throw new PageNotFoundException( 'The param count in the URI are greater than the controller method params.' . ' Handler:' . $this->controller . '::' . $this->method - . ', URI:' . $uri + . ', URI:' . $this->uri ); } } @@ -445,12 +461,12 @@ private function checkRemap(): void 'AutoRouterImproved does not support `_remap()` method.' . ' Controller:' . $this->controller ); - } catch (ReflectionException $e) { + } catch (ReflectionException) { // Do nothing. } } - private function checkUnderscore(string $uri): void + private function checkUnderscore(): void { if ($this->translateURIDashes === false) { return; @@ -459,19 +475,57 @@ private function checkUnderscore(string $uri): void $paramPos = $this->paramPos ?? count($this->segments); for ($i = 0; $i < $paramPos; $i++) { - if (strpos($this->segments[$i], '_') !== false) { + if (str_contains($this->segments[$i], '_')) { throw new PageNotFoundException( 'AutoRouterImproved prohibits access to the URI' . ' containing underscores ("' . $this->segments[$i] . '")' . ' when $translateURIDashes is enabled.' . ' Please use the dash.' . ' Handler:' . $this->controller . '::' . $this->method - . ', URI:' . $uri + . ', URI:' . $this->uri ); } } } + /** + * Check URI for controller for $translateUriToCamelCase + * + * @param string $classname Controller classname that is generated from URI. + * The case may be a bit incorrect. + */ + private function checkUriForController(string $classname): void + { + if ($this->translateUriToCamelCase === false) { + return; + } + + if (! in_array(ltrim($classname, '\\'), get_declared_classes(), true)) { + throw new PageNotFoundException( + '"' . $classname . '" is not found.' + ); + } + } + + /** + * Check URI for method for $translateUriToCamelCase + * + * @param string $method Controller method name that is generated from URI. + * The case may be a bit incorrect. + */ + private function checkUriForMethod(string $method): void + { + if ($this->translateUriToCamelCase === false) { + return; + } + + if (! in_array($method, get_class_methods($this->controller), true)) { + throw new PageNotFoundException( + '"' . $this->controller . '::' . $method . '()" is not found.' + ); + } + } + /** * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment * @@ -484,8 +538,42 @@ private function isValidSegment(string $segment): bool private function translateURIDashes(string $segment): string { - return $this->translateURIDashes - ? str_replace('-', '_', $segment) - : $segment; + if ($this->translateUriToCamelCase) { + if (strtolower($segment) !== $segment) { + throw new PageNotFoundException( + 'AutoRouterImproved prohibits access to the URI' + . ' containing uppercase letters ("' . $segment . '")' + . ' when $translateUriToCamelCase is enabled.' + . ' Please use the dash.' + . ' URI:' . $this->uri + ); + } + + if (str_contains($segment, '--')) { + throw new PageNotFoundException( + 'AutoRouterImproved prohibits access to the URI' + . ' containing double dash ("' . $segment . '")' + . ' when $translateUriToCamelCase is enabled.' + . ' Please use the single dash.' + . ' URI:' . $this->uri + ); + } + + return str_replace( + ' ', + '', + ucwords( + preg_replace('/[\-]+/', ' ', $segment) + ) + ); + } + + $segment = ucfirst($segment); + + if ($this->translateURIDashes) { + return str_replace('-', '_', $segment); + } + + return $segment; } } diff --git a/system/Router/AutoRouterInterface.php b/system/Router/AutoRouterInterface.php index 6d98aec4a5bf..7edec821dbfd 100644 --- a/system/Router/AutoRouterInterface.php +++ b/system/Router/AutoRouterInterface.php @@ -1,5 +1,7 @@ routeCollection = $routes; } /** @@ -33,18 +32,7 @@ public function __construct(RouteCollection $routes) */ public function collect(): Generator { - $methods = [ - 'get', - 'head', - 'post', - 'patch', - 'put', - 'delete', - 'options', - 'trace', - 'connect', - 'cli', - ]; + $methods = Router::HTTP_METHODS; foreach ($methods as $method) { $routes = $this->routeCollection->getRoutes($method); diff --git a/system/Router/Exceptions/MethodNotFoundException.php b/system/Router/Exceptions/MethodNotFoundException.php index d9ad45a5fa3d..6e82fb09583c 100644 --- a/system/Router/Exceptions/MethodNotFoundException.php +++ b/system/Router/Exceptions/MethodNotFoundException.php @@ -1,5 +1,7 @@ [], - 'options' => [], - 'get' => [], - 'head' => [], - 'post' => [], - 'put' => [], - 'delete' => [], - 'trace' => [], - 'connect' => [], - 'cli' => [], + '*' => [], + Method::OPTIONS => [], + Method::GET => [], + Method::HEAD => [], + Method::POST => [], + Method::PATCH => [], + Method::PUT => [], + Method::DELETE => [], + Method::TRACE => [], + Method::CONNECT => [], + 'CLI' => [], ]; /** @@ -161,16 +165,17 @@ class RouteCollection implements RouteCollectionInterface * ] */ protected $routesNames = [ - '*' => [], - 'options' => [], - 'get' => [], - 'head' => [], - 'post' => [], - 'put' => [], - 'delete' => [], - 'trace' => [], - 'connect' => [], - 'cli' => [], + '*' => [], + Method::OPTIONS => [], + Method::GET => [], + Method::HEAD => [], + Method::POST => [], + Method::PATCH => [], + Method::PUT => [], + Method::DELETE => [], + Method::TRACE => [], + Method::CONNECT => [], + 'CLI' => [], ]; /** @@ -191,7 +196,7 @@ class RouteCollection implements RouteCollectionInterface /** * The current method that the script is being called by. * - * @var string HTTP verb (lower case) like `get`,`post` or `*` + * @var string HTTP verb like `GET`,`POST` or `*` or `CLI` */ protected $HTTPVerb = '*'; @@ -199,19 +204,9 @@ class RouteCollection implements RouteCollectionInterface * The default list of HTTP methods (and CLI for command line usage) * that is allowed if no other method is provided. * - * @var array + * @var list */ - protected $defaultHTTPMethods = [ - 'options', - 'get', - 'head', - 'post', - 'put', - 'delete', - 'trace', - 'connect', - 'cli', - ]; + public $defaultHTTPMethods = Router::HTTP_METHODS; /** * The name of the current group, if any. @@ -245,7 +240,7 @@ class RouteCollection implements RouteCollectionInterface /** * Handle to the file locator to use. * - * @var FileLocator + * @var FileLocatorInterface */ protected $fileLocator; @@ -283,7 +278,7 @@ class RouteCollection implements RouteCollectionInterface /** * Constructor */ - public function __construct(FileLocator $locator, Modules $moduleConfig, Routing $routing) + public function __construct(FileLocatorInterface $locator, Modules $moduleConfig, Routing $routing) { $this->fileLocator = $locator; $this->moduleConfig = $moduleConfig; @@ -561,7 +556,7 @@ public function shouldAutoRoute(): bool /** * Returns the raw array of available routes. * - * @param non-empty-string|null $verb + * @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 @@ -607,6 +602,8 @@ public function getRoutes(?string $verb = null, bool $includeWildcard = true): a /** * 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 @@ -634,7 +631,19 @@ public function getHTTPVerb(): string */ public function setHTTPVerb(string $verb) { - $this->HTTPVerb = strtolower($verb); + if ($verb !== '*' && $verb === strtolower($verb)) { + @trigger_error( + 'Passing lowercase HTTP method "' . $verb . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($verb) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @deprecated 4.5.0 + * @TODO Remove strtoupper() in the future. + */ + $this->HTTPVerb = strtoupper($verb); return $this; } @@ -686,10 +695,10 @@ public function addRedirect(string $from, string $to, int $status = 302) $routeName = $to; $routeKey = $this->routesNames['*'][$routeName]; $redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']]; - } elseif (array_key_exists($to, $this->routesNames['get'])) { + } elseif (array_key_exists($to, $this->routesNames[Method::GET])) { $routeName = $to; - $routeKey = $this->routesNames['get'][$routeName]; - $redirectTo = [$routeKey => $this->routes['get'][$routeKey]['handler']]; + $routeKey = $this->routesNames[Method::GET][$routeName]; + $redirectTo = [$routeKey => $this->routes[Method::GET][$routeKey]['handler']]; } else { // The named route is not found. $redirectTo = $to; @@ -772,7 +781,19 @@ public function group(string $name, ...$params) $callback = array_pop($params); if ($params && is_array($params[0])) { - $this->currentOptions = array_shift($params); + $options = array_shift($params); + + if (isset($options['filter'])) { + // Merge filters. + $currentFilter = (array) ($this->currentOptions['filter'] ?? []); + $options['filter'] = array_merge($currentFilter, (array) $options['filter']); + } + + // Merge options other than filters. + $this->currentOptions = array_merge( + $this->currentOptions ?? [], + $options + ); } if (is_callable($callback)) { @@ -991,7 +1012,7 @@ public function presenter(string $name, ?array $options = null): RouteCollection * Specifies a single route to match for multiple HTTP Verbs. * * Example: - * $route->match( ['get', 'post'], 'users/(:num)', 'users/$1); + * $route->match( ['GET', 'POST'], 'users/(:num)', 'users/$1); * * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to */ @@ -1002,6 +1023,18 @@ public function match(array $verbs = [], string $from = '', $to = '', ?array $op } foreach ($verbs as $verb) { + if ($verb === strtolower($verb)) { + @trigger_error( + 'Passing lowercase HTTP method "' . $verb . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($verb) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @TODO We should use correct uppercase verb. + * @deprecated 4.5.0 + */ $verb = strtolower($verb); $this->{$verb}($from, $to, $options); @@ -1017,7 +1050,7 @@ public function match(array $verbs = [], string $from = '', $to = '', ?array $op */ public function get(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('get', $from, $to, $options); + $this->create(Method::GET, $from, $to, $options); return $this; } @@ -1029,7 +1062,7 @@ public function get(string $from, $to, ?array $options = null): RouteCollectionI */ public function post(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('post', $from, $to, $options); + $this->create(Method::POST, $from, $to, $options); return $this; } @@ -1041,7 +1074,7 @@ public function post(string $from, $to, ?array $options = null): RouteCollection */ public function put(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('put', $from, $to, $options); + $this->create(Method::PUT, $from, $to, $options); return $this; } @@ -1053,7 +1086,7 @@ public function put(string $from, $to, ?array $options = null): RouteCollectionI */ public function delete(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('delete', $from, $to, $options); + $this->create(Method::DELETE, $from, $to, $options); return $this; } @@ -1065,7 +1098,7 @@ public function delete(string $from, $to, ?array $options = null): RouteCollecti */ public function head(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('head', $from, $to, $options); + $this->create(Method::HEAD, $from, $to, $options); return $this; } @@ -1077,7 +1110,7 @@ public function head(string $from, $to, ?array $options = null): RouteCollection */ public function patch(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('patch', $from, $to, $options); + $this->create(Method::PATCH, $from, $to, $options); return $this; } @@ -1089,7 +1122,7 @@ public function patch(string $from, $to, ?array $options = null): RouteCollectio */ public function options(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('options', $from, $to, $options); + $this->create(Method::OPTIONS, $from, $to, $options); return $this; } @@ -1101,7 +1134,7 @@ public function options(string $from, $to, ?array $options = null): RouteCollect */ public function cli(string $from, $to, ?array $options = null): RouteCollectionInterface { - $this->create('cli', $from, $to, $options); + $this->create('CLI', $from, $to, $options); return $this; } @@ -1112,14 +1145,14 @@ public function cli(string $from, $to, ?array $options = null): RouteCollectionI */ public function view(string $from, string $view, ?array $options = null): RouteCollectionInterface { - $to = static fn (...$data) => Services::renderer() + $to = static fn (...$data) => service('renderer') ->setData(['segments' => $data], 'raw') ->render($view, $options); $routeOptions = $options ?? []; $routeOptions = array_merge($routeOptions, ['view' => $view]); - $this->create('get', $from, $to, $routeOptions); + $this->create(Method::GET, $from, $to, $routeOptions); return $this; } @@ -1175,8 +1208,8 @@ public function reverseRoute(string $search, ...$params) // Add the default namespace if needed. $namespace = trim($this->defaultNamespace, '\\') . '\\'; if ( - substr($search, 0, 1) !== '\\' - && substr($search, 0, strlen($namespace)) !== $namespace + ! str_starts_with($search, '\\') + && ! str_starts_with($search, $namespace) ) { $search = $namespace . $search; } @@ -1200,7 +1233,7 @@ public function reverseRoute(string $search, ...$params) // If there's any chance of a match, then it will // be with $search at the beginning of the $to string. - if (strpos($to, $search) !== 0) { + if (! str_starts_with($to, $search)) { continue; } @@ -1225,11 +1258,13 @@ public function reverseRoute(string $search, ...$params) */ protected function localizeRoute(string $route): string { - return strtr($route, ['{locale}' => Services::request()->getLocale()]); + 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 { @@ -1238,25 +1273,6 @@ public function isFiltered(string $search, ?string $verb = null): bool return isset($options[$search]['filter']); } - /** - * Returns the filter 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']. - * - * @deprecated Use getFiltersForRoute() - */ - public function getFilterForRoute(string $search, ?string $verb = null): string - { - $options = $this->loadRoutesOptions($verb); - - return $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 @@ -1267,7 +1283,8 @@ public function getFilterForRoute(string $search, ?string $verb = null): string * * has a filter of "role", with parameters of ['admin', 'manager']. * - * @param string $search routeKey + * @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' */ @@ -1338,7 +1355,7 @@ protected function buildReverseRoute(string $from, array $params): string preg_match_all('/\(([^)]+)\)/', $from, $matches); if (empty($matches[0])) { - if (strpos($from, '{locale}') !== false) { + if (str_contains($from, '{locale}')) { $locale = $params[0] ?? null; } @@ -1373,14 +1390,14 @@ protected function buildReverseRoute(string $from, array $params): string // or maybe $placeholder is not a placeholder, but a regex. $pattern = $this->placeholders[$placeholderName] ?? $placeholder; - if (! preg_match('#^' . $pattern . '$#u', $params[$index])) { + if (! preg_match('#^' . $pattern . '$#u', (string) $params[$index])) { throw RouterException::forInvalidParameterType(); } // Ensure that the param we're inserting matches // the expected param type. $pos = strpos($from, $placeholder); - $from = substr_replace($from, $params[$index], $pos, strlen($placeholder)); + $from = substr_replace($from, (string) $params[$index], $pos, strlen($placeholder)); } $from = $this->replaceLocale($from, $locale); @@ -1393,7 +1410,7 @@ protected function buildReverseRoute(string $from, array $params): string */ private function replaceLocale(string $route, ?string $locale = null): string { - if (strpos($route, '{locale}') === false) { + if (! str_contains($route, '{locale}')) { return $route; } @@ -1406,7 +1423,7 @@ private function replaceLocale(string $route, ?string $locale = null): string } if ($locale === null) { - $locale = Services::request()->getLocale(); + $locale = service('request')->getLocale(); } return strtr($route, ['{locale}' => $locale]); @@ -1498,7 +1515,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul // If is redirect, No processing if (! isset($options['redirect']) && is_string($to)) { // If no namespace found, add the default namespace - if (strpos($to, '\\') === false || strpos($to, '\\') > 0) { + if (! str_contains($to, '\\') || strpos($to, '\\') > 0) { $namespace = $options['namespace'] ?? $this->defaultNamespace; $to = trim($namespace, '\\') . '\\' . $to; } @@ -1637,7 +1654,7 @@ private function determineCurrentSubdomain() // on the URL else parse_url will mis-interpret // 'host' as the 'path'. $url = $this->httpHost; - if (strpos($url, 'http') !== 0) { + if (! str_starts_with($url, 'http')) { $url = 'http://' . $url; } @@ -1736,12 +1753,29 @@ public function setPrioritize(bool $enabled = true) /** * Get all controllers in Route Handlers * - * @param string|null $verb HTTP verb. `'*'` returns all controllers in any verb. + * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. + * `'*'` returns all controllers in any verb. * * @return list controller name list + * + * @interal */ public function getRegisteredControllers(?string $verb = '*'): array { + if ($verb !== '*' && $verb === strtolower($verb)) { + @trigger_error( + 'Passing lowercase HTTP method "' . $verb . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($verb) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @deprecated 4.5.0 + * @TODO Remove this in the future. + */ + $verb = strtoupper($verb); + $controllers = []; if ($verb === '*') { diff --git a/system/Router/RouteCollectionInterface.php b/system/Router/RouteCollectionInterface.php index c9c8610009fc..bc6ca022bf93 100644 --- a/system/Router/RouteCollectionInterface.php +++ b/system/Router/RouteCollectionInterface.php @@ -1,5 +1,7 @@ collection->getDefaultNamespace(), $this->collection->getDefaultController(), $this->collection->getDefaultMethod(), - $this->translateURIDashes, - $this->collection->getHTTPVerb() + $this->translateURIDashes ); } else { $this->autoRouter = new AutoRouter( - $this->collection->getRoutes('cli', false), // @phpstan-ignore-line + $this->collection->getRoutes('CLI', false), // @phpstan-ignore-line $this->collection->getDefaultNamespace(), $this->collection->getDefaultController(), $this->collection->getDefaultMethod(), - $this->translateURIDashes, - $this->collection->getHTTPVerb() + $this->translateURIDashes ); } } @@ -195,19 +203,12 @@ public function handle(?string $uri = null) $this->checkDisallowedChars($uri); // Restart filterInfo - $this->filterInfo = null; $this->filtersInfo = []; // Checks defined routes if ($this->checkRoutes($uri)) { if ($this->collection->isFiltered($this->matchedRoute[0])) { - $multipleFiltersEnabled = config(Feature::class)->multipleFilters ?? false; - if ($multipleFiltersEnabled) { - $this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]); - } else { - // for backward compatibility - $this->filterInfo = $this->collection->getFilterForRoute($this->matchedRoute[0]); - } + $this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]); } return $this->controller; @@ -228,18 +229,6 @@ public function handle(?string $uri = null) return $this->controllerName(); } - /** - * Returns the filter info for the matched route, if any. - * - * @return string|null - * - * @deprecated Use getFilters() - */ - public function getFilter() - { - return $this->filterInfo; - } - /** * Returns the filter info for the matched route, if any. * @@ -415,7 +404,7 @@ protected function checkRoutes(string $uri): bool $routes = $this->collection->getRoutes($this->collection->getHTTPVerb()); // Don't waste any time - if (empty($routes)) { + if ($routes === []) { return false; } @@ -427,12 +416,14 @@ protected function checkRoutes(string $uri): bool foreach ($routes as $routeKey => $handler) { $routeKey = $routeKey === '/' ? $routeKey - : ltrim($routeKey, '/ '); + // $routeKey may be int, because it is an array key, + // and the URI `/1` is valid. The leading `/` is removed. + : ltrim((string) $routeKey, '/ '); $matchedKey = $routeKey; // Are we dealing with a locale? - if (strpos($routeKey, '{locale}') !== false) { + if (str_contains($routeKey, '{locale}')) { $routeKey = str_replace('{locale}', '[^/]+', $routeKey); } @@ -454,7 +445,7 @@ protected function checkRoutes(string $uri): bool } // Store our locale so CodeIgniter object can // assign it to the Request. - if (strpos($matchedKey, '{locale}') !== false) { + if (str_contains($matchedKey, '{locale}')) { preg_match( '#^' . str_replace('{locale}', '(?[^/]+)', $matchedKey) . '$#u', $uri, @@ -488,24 +479,47 @@ protected function checkRoutes(string $uri): bool return true; } - [$controller] = explode('::', $handler); + if (str_contains($handler, '::')) { + [$controller, $methodAndParams] = explode('::', $handler); + } else { + $controller = $handler; + $methodAndParams = ''; + } // Checks `/` in controller name - if (strpos($controller, '/') !== false) { + if (str_contains($controller, '/')) { throw RouterException::forInvalidControllerName($handler); } - if (strpos($handler, '$') !== false && strpos($routeKey, '(') !== false) { + if (str_contains($handler, '$') && str_contains($routeKey, '(')) { // Checks dynamic controller - if (strpos($controller, '$') !== false) { + if (str_contains($controller, '$')) { throw RouterException::forDynamicController($handler); } - // Using back-references - $handler = preg_replace('#\A' . $routeKey . '\z#u', $handler, $uri); + if (config(Routing::class)->multipleSegmentsOneParam === false) { + // Using back-references + $segments = explode('/', preg_replace('#\A' . $routeKey . '\z#u', $handler, $uri)); + } else { + if (str_contains($methodAndParams, '/')) { + [$method, $handlerParams] = explode('/', $methodAndParams, 2); + $params = explode('/', $handlerParams); + $handlerSegments = array_merge([$controller . '::' . $method], $params); + } else { + $handlerSegments = [$handler]; + } + + $segments = []; + + foreach ($handlerSegments as $segment) { + $segments[] = $this->replaceBackReferences($segment, $matches); + } + } + } else { + $segments = explode('/', $handler); } - $this->setRequest(explode('/', $handler)); + $this->setRequest($segments); $this->setMatchedRoute($matchedKey, $handler); @@ -516,6 +530,24 @@ protected function checkRoutes(string $uri): bool return false; } + /** + * Replace string `$n` with `$matches[n]` value. + */ + private function replaceBackReferences(string $input, array $matches): string + { + $pattern = '/\$([1-' . count($matches) . '])/u'; + + return preg_replace_callback( + $pattern, + static function ($match) use ($matches) { + $index = (int) $match[1]; + + return $matches[$index] ?? ''; + }, + $input + ); + } + /** * Checks Auto Routes. * diff --git a/system/Router/RouterInterface.php b/system/Router/RouterInterface.php index ccdef3d6d9be..c50de86d43df 100644 --- a/system/Router/RouterInterface.php +++ b/system/Router/RouterInterface.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Security; + +use CodeIgniter\CLI\CLI; +use CodeIgniter\View\Table; + +/** + * Checks php.ini settings + * + * @used-by \CodeIgniter\Commands\Utilities\PhpIniCheck + * @see \CodeIgniter\Security\CheckPhpIniTest + */ +class CheckPhpIni +{ + /** + * @param bool $isCli Set false if you run via Web + * + * @return string|void HTML string or void in CLI + */ + public static function run(bool $isCli = true) + { + $output = static::checkIni(); + + $thead = ['Directive', 'Global', 'Current', 'Recommended', 'Remark']; + $tbody = []; + + // CLI + if ($isCli) { + self::outputForCli($output, $thead, $tbody); + + return; + } + + // Web + return self::outputForWeb($output, $thead, $tbody); + } + + private static function outputForCli(array $output, array $thead, array $tbody): void + { + foreach ($output as $directive => $values) { + $current = $values['current']; + $notRecommended = false; + + if ($values['recommended'] !== '') { + if ($values['recommended'] !== $values['current']) { + $notRecommended = true; + } + + $current = $notRecommended + ? CLI::color($values['current'] === '' ? 'n/a' : $values['current'], 'red') + : $values['current']; + } + + $directive = $notRecommended ? CLI::color($directive, 'red') : $directive; + $tbody[] = [ + $directive, $values['global'], $current, $values['recommended'], $values['remark'], + ]; + } + + CLI::table($tbody, $thead); + } + + private static function outputForWeb(array $output, array $thead, array $tbody): string + { + foreach ($output as $directive => $values) { + $current = $values['current']; + $notRecommended = false; + + if ($values['recommended'] !== '') { + if ($values['recommended'] !== $values['current']) { + $notRecommended = true; + } + + if ($values['current'] === '') { + $current = 'n/a'; + } + + $current = $notRecommended + ? '' . $current . '' + : $current; + } + + $directive = $notRecommended + ? '' . $directive . '' + : $directive; + $tbody[] = [ + $directive, $values['global'], $current, $values['recommended'], $values['remark'], + ]; + } + + $table = new Table(); + $template = [ + 'table_open' => '
getHeaderLine($name), 'html') ?> + getHeaderLine($name), 'html'); + } else { + foreach ($value as $i => $header) { + echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html'); + } + } + ?> +
', + ]; + $table->setTemplate($template); + + $table->setHeading($thead); + + return '
' . $table->generate($tbody) . '
'; + } + + /** + * @internal Used for testing purposes only. + * @testTag + */ + public static function checkIni(): array + { + $items = [ + 'error_reporting' => ['recommended' => '5111'], + 'display_errors' => ['recommended' => '0'], + 'display_startup_errors' => ['recommended' => '0'], + 'log_errors' => [], + 'error_log' => [], + 'default_charset' => ['recommended' => 'UTF-8'], + 'memory_limit' => ['remark' => '> post_max_size'], + 'post_max_size' => ['remark' => '> upload_max_filesize'], + 'upload_max_filesize' => ['remark' => '< post_max_size'], + 'request_order' => ['recommended' => 'GP'], + 'variables_order' => ['recommended' => 'GPCS'], + 'date.timezone' => ['recommended' => 'UTC'], + 'mbstring.language' => ['recommended' => 'neutral'], + 'opcache.enable' => ['recommended' => '1'], + 'opcache.enable_cli' => [], + 'opcache.jit' => [], + 'opcache.jit_buffer_size' => [], + ]; + + $output = []; + $ini = ini_get_all(); + + foreach ($items as $key => $values) { + $output[$key] = [ + 'global' => $ini[$key]['global_value'], + 'current' => $ini[$key]['local_value'], + 'recommended' => $values['recommended'] ?? '', + 'remark' => $values['remark'] ?? '', + ]; + } + + // [directive => [current_value, recommended_value]] + return $output; + } +} diff --git a/system/Security/Exceptions/SecurityException.php b/system/Security/Exceptions/SecurityException.php index ab2f6adc7f43..16383fc25bfc 100644 --- a/system/Security/Exceptions/SecurityException.php +++ b/system/Security/Exceptions/SecurityException.php @@ -1,5 +1,7 @@ configureSession(); } - $this->request = Services::request(); + $this->request = service('request'); $this->hashInCookie = $this->request->getCookie($this->cookieName); $this->restoreHash(); @@ -220,7 +222,7 @@ private function isCSRFCookie(): bool private function configureSession(): void { - $this->session = Services::session(); + $this->session = service('session'); } private function configureCookie(CookieConfig $cookie): void @@ -230,46 +232,6 @@ private function configureCookie(CookieConfig $cookie): void Cookie::setDefaults($cookie); } - /** - * CSRF Verify - * - * @return $this|false - * - * @throws SecurityException - * - * @deprecated Use `CodeIgniter\Security\Security::verify()` instead of using this method. - * - * @codeCoverageIgnore - */ - public function CSRFVerify(RequestInterface $request) - { - return $this->verify($request); - } - - /** - * Returns the CSRF Token. - * - * @deprecated Use `CodeIgniter\Security\Security::getHash()` instead of using this method. - * - * @codeCoverageIgnore - */ - public function getCSRFHash(): ?string - { - return $this->getHash(); - } - - /** - * Returns the CSRF Token Name. - * - * @deprecated Use `CodeIgniter\Security\Security::getTokenName()` instead of using this method. - * - * @codeCoverageIgnore - */ - public function getCSRFTokenName(): string - { - return $this->getTokenName(); - } - /** * CSRF Verify * @@ -280,8 +242,8 @@ public function getCSRFTokenName(): string public function verify(RequestInterface $request) { // Protects POST, PUT, DELETE, PATCH - $method = strtoupper($request->getMethod()); - $methodsToProtect = ['POST', 'PUT', 'DELETE', 'PATCH']; + $method = $request->getMethod(); + $methodsToProtect = [Method::POST, Method::PUT, Method::DELETE, Method::PATCH]; if (! in_array($method, $methodsToProtect, true)) { return $this; } @@ -291,7 +253,7 @@ public function verify(RequestInterface $request) try { $token = ($postedToken !== null && $this->config->tokenRandomize) ? $this->derandomize($postedToken) : $postedToken; - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { $token = null; } @@ -443,18 +405,6 @@ public function getCookieName(): string return $this->config->cookieName; } - /** - * Check if CSRF cookie is expired. - * - * @deprecated - * - * @codeCoverageIgnore - */ - public function isExpired(): bool - { - return $this->cookie->isExpired(); - } - /** * Check if request should be redirect on failure. */ @@ -583,44 +533,10 @@ private function saveHashInCookie(): void ] ); - $response = Services::response(); + $response = service('response'); $response->setCookie($this->cookie); } - /** - * CSRF Send Cookie - * - * @return false|Security - * - * @deprecated Set cookies to Response object instead. - */ - protected function sendCookie(RequestInterface $request) - { - assert($request instanceof IncomingRequest); - - if ($this->cookie->isSecure() && ! $request->isSecure()) { - return false; - } - - $this->doSendCookie(); - log_message('info', 'CSRF cookie sent.'); - - return $this; - } - - /** - * Actual dispatching of cookies. - * Extracted for this to be unit tested. - * - * @codeCoverageIgnore - * - * @deprecated Set cookies to Response object instead. - */ - protected function doSendCookie(): void - { - cookies([$this->cookie], false)->dispatch(); - } - private function saveHashInSession(): void { $this->session->set($this->config->tokenName, $this->hash); diff --git a/system/Security/SecurityInterface.php b/system/Security/SecurityInterface.php index af731e82789a..1460cf0e6a00 100644 --- a/system/Security/SecurityInterface.php +++ b/system/Security/SecurityInterface.php @@ -1,5 +1,7 @@ sessionExpiration = ($config->expiration === 0) ? (int) ini_get('session.gc_maxlifetime') : $config->expiration; + // Add sessionCookieName for multiple session cookies. $this->keyPrefix .= $config->cookieName . ':'; @@ -82,6 +95,9 @@ public function __construct(SessionConfig $config, string $ipAddress) if ($this->matchIP === true) { $this->keyPrefix .= $this->ipAddress . ':'; } + + $this->lockRetryInterval = $config->lockWait ?? $this->lockRetryInterval; + $this->lockMaxRetries = $config->lockAttempts ?? $this->lockMaxRetries; } protected function setSavePath(): void @@ -90,23 +106,57 @@ protected function setSavePath(): void throw SessionException::forEmptySavepath(); } - if (preg_match('#(?:(tcp|tls)://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->savePath, $matches)) { - if (! isset($matches[4])) { - $matches[4] = ''; // Just to avoid undefined index notices below - } + $url = parse_url($this->savePath); + $query = []; - $this->savePath = [ - 'protocol' => ! empty($matches[1]) ? $matches[1] : self::DEFAULT_PROTOCOL, - 'host' => $matches[2], - 'port' => empty($matches[3]) ? self::DEFAULT_PORT : $matches[3], - 'password' => preg_match('#auth=([^\s&]+)#', $matches[4], $match) ? $match[1] : null, - 'database' => preg_match('#database=(\d+)#', $matches[4], $match) ? (int) $match[1] : 0, - 'timeout' => preg_match('#timeout=(\d+\.\d+|\d+)#', $matches[4], $match) ? (float) $match[1] : 0.0, - ]; + if ($url === false) { + // Unix domain socket like `unix:///var/run/redis/redis.sock?persistent=1`. + if (preg_match('#unix://(/[^:?]+)(\?.+)?#', $this->savePath, $matches)) { + $host = $matches[1]; + $port = 0; - preg_match('#prefix=([^\s&]+)#', $matches[4], $match) && $this->keyPrefix = $match[1]; + if (isset($matches[2])) { + parse_str(ltrim($matches[2], '?'), $query); + } + } else { + throw SessionException::forInvalidSavePathFormat($this->savePath); + } } else { - throw SessionException::forInvalidSavePathFormat($this->savePath); + // Also accepts `/var/run/redis.sock` for backward compatibility. + if (isset($url['path']) && $url['path'][0] === '/') { + $host = $url['path']; + $port = 0; + } else { + // TCP connection. + if (! isset($url['host'])) { + throw SessionException::forInvalidSavePathFormat($this->savePath); + } + + $protocol = $url['scheme'] ?? self::DEFAULT_PROTOCOL; + $host = $protocol . '://' . $url['host']; + $port = $url['port'] ?? self::DEFAULT_PORT; + } + + if (isset($url['query'])) { + parse_str($url['query'], $query); + } + } + + $password = $query['auth'] ?? null; + $database = isset($query['database']) ? (int) $query['database'] : 0; + $timeout = isset($query['timeout']) ? (float) $query['timeout'] : 0.0; + $prefix = $query['prefix'] ?? null; + + $this->savePath = [ + 'host' => $host, + 'port' => $port, + 'password' => $password, + 'database' => $database, + 'timeout' => $timeout, + ]; + + if ($prefix !== null) { + $this->keyPrefix = $prefix; } } @@ -128,8 +178,8 @@ public function open($path, $name): bool if ( ! $redis->connect( - $this->savePath['protocol'] . '://' . $this->savePath['host'], - ($this->savePath['host'][0] === '/' ? 0 : $this->savePath['port']), + $this->savePath['host'], + $this->savePath['port'], $this->savePath['timeout'] ) ) { @@ -323,14 +373,14 @@ protected function lockSession(string $sessionID): bool ); if (! $result) { - usleep(100000); + usleep($this->lockRetryInterval); continue; } $this->lockKey = $lockKey; break; - } while (++$attempt < 300); + } while (++$attempt < $this->lockMaxRetries); if ($attempt === 300) { $this->logger->error( diff --git a/system/Session/Session.php b/system/Session/Session.php index 875387f29827..0aabcbe31d53 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -1,5 +1,7 @@ getCookieStore(); if (! $cookieStoreInResponse->has($this->config->cookieName)) { @@ -929,7 +930,7 @@ protected function setCookie() $expiration = $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration; $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration); - $response = Services::response(); + $response = service('response'); $response->setCookie($this->cookie); } } diff --git a/system/Session/SessionInterface.php b/system/Session/SessionInterface.php index 424834c79639..3ed61664a952 100644 --- a/system/Session/SessionInterface.php +++ b/system/Session/SessionInterface.php @@ -1,5 +1,7 @@ assertLessThanOrEqual($tolerance, $difference, $message); - } catch (Exception $e) { + } catch (Exception) { return false; } } @@ -495,7 +497,7 @@ public function assertCloseEnoughString($expected, $actual, string $message = '' protected function createApplication() { // Initialize the autoloader. - Services::autoloader()->initialize(new Autoload(), new Modules()); + service('autoloader')->initialize(new Autoload(), new Modules()); $app = new MockCodeIgniter(new App()); $app->initialize(); @@ -515,7 +517,7 @@ protected function getHeaderEmitted(string $header, bool $ignoreCase = false, st foreach (xdebug_get_headers() as $emittedHeader) { $found = $ignoreCase ? (stripos($emittedHeader, $header) === 0) - : (strpos($emittedHeader, $header) === 0); + : (str_starts_with($emittedHeader, $header)); if ($found) { return $emittedHeader; diff --git a/system/Test/ConfigFromArrayTrait.php b/system/Test/ConfigFromArrayTrait.php index 17d2dd553cfe..3652fa1074a1 100644 --- a/system/Test/ConfigFromArrayTrait.php +++ b/system/Test/ConfigFromArrayTrait.php @@ -1,5 +1,7 @@ toString(JSON_PRETTY_PRINT), + $this->toString(false, JSON_PRETTY_PRINT), $this->getAdditionalInfo($table) ); } @@ -111,7 +113,7 @@ protected function getAdditionalInfo(string $table): string * * @param int $options */ - public function toString($options = 0): string + public function toString(bool $exportObjects = false, $options = 0): string { return json_encode($this->data, $options); } diff --git a/system/Test/ControllerResponse.php b/system/Test/ControllerResponse.php deleted file mode 100644 index 2a90e4dd0bb9..000000000000 --- a/system/Test/ControllerResponse.php +++ /dev/null @@ -1,99 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Test; - -use CodeIgniter\HTTP\ResponseInterface; -use Config\Services; - -/** - * Testable response from a controller - * - * @deprecated Use TestResponse directly - * - * @codeCoverageIgnore - */ -class ControllerResponse extends TestResponse -{ - /** - * The message payload. - * - * @var string - * - * @deprecated Use $response->getBody() instead - */ - protected $body; - - /** - * DOM for the body. - * - * @var DOMParser - * - * @deprecated Use $domParser instead - */ - protected $dom; - - /** - * Maintains the deprecated $dom property. - */ - public function __construct() - { - parent::__construct(Services::response()); - - $this->dom = &$this->domParser; - } - - /** - * Sets the response. - * - * @return $this - * - * @deprecated Will revert to parent::setResponse() in a future release (no $body updates) - */ - public function setResponse(ResponseInterface $response) - { - parent::setResponse($response); - - $this->body = $response->getBody() ?? ''; - - return $this; - } - - /** - * Sets the body and updates the DOM. - * - * @return $this - * - * @deprecated Use response()->setBody() instead - */ - public function setBody(string $body) - { - $this->body = $body; - - if ($body !== '') { - $this->domParser->withString($body); - } - - return $this; - } - - /** - * Retrieve the body. - * - * @return string - * - * @deprecated Use response()->getBody() instead - */ - public function getBody() - { - return $this->body; - } -} diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index a99b899eaf63..ab1a1aef3907 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -100,13 +100,13 @@ protected function setUpControllerTestTrait(): void } if (! $this->uri instanceof URI) { - $factory = Services::siteurifactory($this->appConfig, Services::superglobals(), false); + $factory = Services::siteurifactory($this->appConfig, service('superglobals'), false); $this->uri = $factory->createFromGlobals(); } if (empty($this->request)) { // Do some acrobatics, so we can use the Request service with our own URI - $tempUri = Services::uri(); + $tempUri = service('uri'); Services::injectMock('uri', $this->uri); $this->withRequest(Services::incomingrequest($this->appConfig, false)); @@ -120,7 +120,7 @@ protected function setUpControllerTestTrait(): void } if (empty($this->logger)) { - $this->logger = Services::logger(); + $this->logger = service('logger'); } } @@ -161,6 +161,8 @@ public function execute(string $method, ...$params) try { ob_start(); + // The controller method param types may not be string. + // So cannot set `declare(strict_types=1)` in this file. $response = $this->controller->{$method}(...$params); } catch (Throwable $e) { $code = $e->getCode(); @@ -202,7 +204,7 @@ public function execute(string $method, ...$params) // getStatusCode() throws for empty codes try { $response->getStatusCode(); - } catch (HTTPException $e) { + } catch (HTTPException) { // If no code has been set then assume success $response->setStatusCode(200); } @@ -278,7 +280,7 @@ public function withLogger($logger) */ public function withUri(string $uri) { - $factory = Services::siteurifactory(); + $factory = service('siteurifactory'); $this->uri = $factory->createFromString($uri); Services::injectMock('uri', $this->uri); diff --git a/system/Test/ControllerTester.php b/system/Test/ControllerTester.php deleted file mode 100644 index c01e6a8ba2f3..000000000000 --- a/system/Test/ControllerTester.php +++ /dev/null @@ -1,293 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Test; - -use CodeIgniter\Controller; -use CodeIgniter\HTTP\IncomingRequest; -use CodeIgniter\HTTP\ResponseInterface; -use CodeIgniter\HTTP\URI; -use Config\App; -use Config\Services; -use InvalidArgumentException; -use Psr\Log\LoggerInterface; -use Throwable; - -/** - * ControllerTester Trait - * - * Provides features that make testing controllers simple and fluent. - * - * Example: - * - * $this->withRequest($request) - * ->withResponse($response) - * ->withURI($uri) - * ->withBody($body) - * ->controller('App\Controllers\Home') - * ->execute('methodName'); - * - * @deprecated Use ControllerTestTrait instead - * - * @codeCoverageIgnore - */ -trait ControllerTester -{ - /** - * Controller configuration. - * - * @var App - */ - protected $appConfig; - - /** - * Request. - * - * @var IncomingRequest - */ - protected $request; - - /** - * Response. - * - * @var ResponseInterface - */ - protected $response; - - /** - * Message logger. - * - * @var LoggerInterface - */ - protected $logger; - - /** - * Initialized controller. - * - * @var Controller - */ - protected $controller; - - /** - * URI of this request. - * - * @var string - */ - protected $uri = 'http://example.com'; - - /** - * Request or response body. - * - * @var string|null - */ - protected $body; - - /** - * Initializes required components. - */ - protected function setUpControllerTester(): void - { - if (empty($this->appConfig)) { - $this->appConfig = config(App::class); - } - - if (! $this->uri instanceof URI) { - $this->uri = Services::uri($this->appConfig->baseURL ?? 'http://example.com/', false); - } - - if (empty($this->request)) { - // Do some acrobatics so we can use the Request service with our own URI - $tempUri = Services::uri(); - Services::injectMock('uri', $this->uri); - - $this->withRequest(Services::incomingrequest($this->appConfig, false)->setBody($this->body)); - - // Restore the URI service - Services::injectMock('uri', $tempUri); - } - - if (empty($this->response)) { - $this->response = Services::response($this->appConfig, false); - } - - if (empty($this->logger)) { - $this->logger = Services::logger(); - } - } - - /** - * Loads the specified controller, and generates any needed dependencies. - * - * @return mixed - */ - public function controller(string $name) - { - if (! class_exists($name)) { - throw new InvalidArgumentException('Invalid Controller: ' . $name); - } - - $this->controller = new $name(); - $this->controller->initController($this->request, $this->response, $this->logger); - - return $this; - } - - /** - * Runs the specified method on the controller and returns the results. - * - * @param array $params - * - * @return ControllerResponse - * - * @throws InvalidArgumentException - */ - public function execute(string $method, ...$params) - { - if (! method_exists($this->controller, $method) || ! is_callable([$this->controller, $method])) { - throw new InvalidArgumentException('Method does not exist or is not callable in controller: ' . $method); - } - - // The URL helper is always loaded by the system - // so ensure it's available. - helper('url'); - - $result = (new ControllerResponse()) - ->setRequest($this->request) - ->setResponse($this->response); - - $response = null; - - try { - ob_start(); - - $response = $this->controller->{$method}(...$params); - } catch (Throwable $e) { - $code = $e->getCode(); - - // If code is not a valid HTTP status then assume there is an error - if ($code < 100 || $code >= 600) { - throw $e; - } - - $result->response()->setStatusCode($code); - } finally { - $output = ob_get_clean(); - - // If the controller returned a response, use it - if (isset($response) && $response instanceof ResponseInterface) { - $result->setResponse($response); - } - - // check if controller returned a view rather than echoing it - if (is_string($response)) { - $output = $response; - $result->response()->setBody($output); - $result->setBody($output); - } elseif (! empty($response) && ! empty($response->getBody())) { - $result->setBody($response->getBody()); - } else { - $result->setBody(''); - } - } - - // If not response code has been sent, assume a success - if (empty($result->response()->getStatusCode())) { - $result->response()->setStatusCode(200); - } - - return $result; - } - - /** - * Set controller's config, with method chaining. - * - * @param mixed $appConfig - * - * @return mixed - */ - public function withConfig($appConfig) - { - $this->appConfig = $appConfig; - - return $this; - } - - /** - * Set controller's request, with method chaining. - * - * @param mixed $request - * - * @return mixed - */ - public function withRequest($request) - { - $this->request = $request; - - // Make sure it's available for other classes - Services::injectMock('request', $request); - - return $this; - } - - /** - * Set controller's response, with method chaining. - * - * @param mixed $response - * - * @return mixed - */ - public function withResponse($response) - { - $this->response = $response; - - return $this; - } - - /** - * Set controller's logger, with method chaining. - * - * @param mixed $logger - * - * @return mixed - */ - public function withLogger($logger) - { - $this->logger = $logger; - - return $this; - } - - /** - * Set the controller's URI, with method chaining. - * - * @return mixed - */ - public function withUri(string $uri) - { - $this->uri = new URI($uri); - - return $this; - } - - /** - * Set the method's body, with method chaining. - * - * @param string|null $body - * - * @return mixed - */ - public function withBody($body) - { - $this->body = $body; - - return $this; - } -} diff --git a/system/Test/DOMParser.php b/system/Test/DOMParser.php index 1565452159f9..fe9ee1d58b2a 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -1,5 +1,7 @@ length; } + /** + * Checks to see if the XPath can be found. + */ + public function seeXPath(string $path): bool + { + $xpath = new DOMXPath($this->dom); + + return (bool) $xpath->query($path)->length; + } + + /** + * Checks to see if the XPath can't be found. + */ + public function dontSeeXPath(string $path): bool + { + return ! $this->seeXPath($path); + } + /** * Search the DOM using an XPath expression. * @@ -240,11 +260,11 @@ public function parseSelector(string $selector) $attr = null; // ID? - if (strpos($selector, '#') !== false) { + if (str_contains($selector, '#')) { [$tag, $id] = explode('#', $selector); } // Attribute - elseif (strpos($selector, '[') !== false && strpos($selector, ']') !== false) { + elseif (str_contains($selector, '[') && str_contains($selector, ']')) { $open = strpos($selector, '['); $close = strpos($selector, ']'); @@ -262,7 +282,7 @@ public function parseSelector(string $selector) $attr = [$name => trim($value, '] ')]; } // Class? - elseif (strpos($selector, '.') !== false) { + elseif (str_contains($selector, '.')) { [$tag, $class] = explode('.', $selector); } // Otherwise, assume the entire string is our tag diff --git a/system/Test/DatabaseTestTrait.php b/system/Test/DatabaseTestTrait.php index 8860458cb089..863ed542319d 100644 --- a/system/Test/DatabaseTestTrait.php +++ b/system/Test/DatabaseTestTrait.php @@ -1,5 +1,7 @@ db)); + $builderClass = str_replace('Connection', 'Builder', $this->db::class); return new $builderClass($tableName, $this->db); } diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index c247b8741b01..8da0c60fc089 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -1,5 +1,7 @@ , + * optional: array, + * valid: array + * } + */ + private array $modifiedFields = ['unique' => [], 'optional' => [], 'valid' => []]; + /** * Default formatter to use when nothing is detected * @@ -249,6 +263,46 @@ public function setOverrides(array $overrides = [], $persist = true): self return $this; } + /** + * Set a field to be unique. + * + * @param bool $reset If set to true, resets the list of existing values + * @param int $maxRetries Maximum number of retries to find a unique value, + * After which an OverflowException is thrown. + */ + public function setUnique(string $field, bool $reset = false, int $maxRetries = 10000): static + { + $this->modifiedFields['unique'][$field] = compact('reset', 'maxRetries'); + + return $this; + } + + /** + * Set a field to be optional. + * + * @param float $weight A probability between 0 and 1, 0 means that we always get the default value. + */ + public function setOptional(string $field, float $weight = 0.5, mixed $default = null): static + { + $this->modifiedFields['optional'][$field] = compact('weight', 'default'); + + return $this; + } + + /** + * Set a field to be valid using a callback. + * + * @param Closure(mixed): bool|null $validator A function returning true for valid values + * @param int $maxRetries Maximum number of retries to find a valid value, + * After which an OverflowException is thrown. + */ + public function setValid(string $field, ?Closure $validator = null, int $maxRetries = 10000): static + { + $this->modifiedFields['valid'][$field] = compact('validator', 'maxRetries'); + + return $this; + } + /** * Returns the current formatters */ @@ -305,7 +359,7 @@ protected function guessFormatter($field): string $this->faker->getFormatter($field); return $field; - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // No match, keep going } @@ -378,7 +432,30 @@ public function makeArray() $result = []; foreach ($this->formatters as $field => $formatter) { - $result[$field] = $this->faker->{$formatter}(); + $faker = $this->faker; + + if (isset($this->modifiedFields['unique'][$field])) { + $faker = $faker->unique( + $this->modifiedFields['unique'][$field]['reset'], + $this->modifiedFields['unique'][$field]['maxRetries'] + ); + } + + if (isset($this->modifiedFields['optional'][$field])) { + $faker = $faker->optional( + $this->modifiedFields['optional'][$field]['weight'], + $this->modifiedFields['optional'][$field]['default'] + ); + } + + if (isset($this->modifiedFields['valid'][$field])) { + $faker = $faker->valid( + $this->modifiedFields['valid'][$field]['validator'], + $this->modifiedFields['valid'][$field]['maxRetries'] + ); + } + + $result[$field] = $faker->format($formatter); } } // If no formatters were defined then look for a model fake() method @@ -497,18 +574,11 @@ public function create(?int $count = null, bool $mock = false) */ protected function createMock(?int $count = null) { - switch ($this->model->dateFormat) { - case 'datetime': - $datetime = date('Y-m-d H:i:s'); - break; - - case 'date': - $datetime = date('Y-m-d'); - break; - - default: - $datetime = Time::now()->getTimestamp(); - } + $datetime = match ($this->model->dateFormat) { + 'datetime' => date('Y-m-d H:i:s'), + 'date' => date('Y-m-d'), + default => Time::now()->getTimestamp(), + }; // Determine which fields we will need $fields = []; diff --git a/system/Test/FeatureResponse.php b/system/Test/FeatureResponse.php deleted file mode 100644 index 971f41c5b6e4..000000000000 --- a/system/Test/FeatureResponse.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Test; - -/** - * Assertions for a response - * - * @deprecated Use TestResponse directly - */ -class FeatureResponse extends TestResponse -{ - /** - * @deprecated Will be protected in a future release, use response() instead - */ - public $response; -} diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php deleted file mode 100644 index fff624a840f3..000000000000 --- a/system/Test/FeatureTestCase.php +++ /dev/null @@ -1,395 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Test; - -use CodeIgniter\Events\Events; -use CodeIgniter\HTTP\CLIRequest; -use CodeIgniter\HTTP\Exceptions\RedirectException; -use CodeIgniter\HTTP\IncomingRequest; -use CodeIgniter\HTTP\URI; -use CodeIgniter\HTTP\UserAgent; -use Config\App; -use Config\Services; -use Exception; -use ReflectionException; - -/** - * Class FeatureTestCase - * - * Provides a base class with the trait for doing full HTTP testing - * against your application. - * - * @no-final - * - * @deprecated Use FeatureTestTrait instead - * - * @codeCoverageIgnore - * - * @internal - */ -class FeatureTestCase extends CIUnitTestCase -{ - use DatabaseTestTrait; - - /** - * Sets a RouteCollection that will override - * the application's route collection. - * - * Example routes: - * [ - * ['get', 'home', 'Home::index'] - * ] - * - * @return $this - */ - protected function withRoutes(?array $routes = null) - { - $collection = Services::routes(); - - if ($routes !== null) { - $collection->resetRoutes(); - - foreach ($routes as $route) { - $collection->{$route[0]}($route[1], $route[2]); - } - } - - $this->routes = $collection; - - return $this; - } - - /** - * Sets any values that should exist during this session. - * - * @param array|null $values Array of values, or null to use the current $_SESSION - * - * @return $this - */ - public function withSession(?array $values = null) - { - $this->session = $values ?? $_SESSION; - - return $this; - } - - /** - * Set request's headers - * - * Example of use - * withHeaders([ - * 'Authorization' => 'Token' - * ]) - * - * @param array $headers Array of headers - * - * @return $this - */ - public function withHeaders(array $headers = []) - { - $this->headers = $headers; - - return $this; - } - - /** - * Set the format the request's body should have. - * - * @param string $format The desired format. Currently supported formats: xml, json - * - * @return $this - */ - public function withBodyFormat(string $format) - { - $this->bodyFormat = $format; - - return $this; - } - - /** - * Set the raw body for the request - * - * @param mixed $body - * - * @return $this - */ - public function withBody($body) - { - $this->requestBody = $body; - - return $this; - } - - /** - * Don't run any events while running this test. - * - * @return $this - */ - public function skipEvents() - { - Events::simulate(true); - - return $this; - } - - /** - * Calls a single URI, executes it, and returns a FeatureResponse - * instance that can be used to run many assertions against. - * - * @return FeatureResponse - */ - public function call(string $method, string $path, ?array $params = null) - { - $buffer = \ob_get_level(); - - // Clean up any open output buffers - // not relevant to unit testing - if (\ob_get_level() > 0 && (! isset($this->clean) || $this->clean === true)) { - \ob_end_clean(); // @codeCoverageIgnore - } - - // Simulate having a blank session - $_SESSION = []; - $_SERVER['REQUEST_METHOD'] = $method; - - $request = $this->setupRequest($method, $path); - $request = $this->setupHeaders($request); - $request = $this->populateGlobals($method, $request, $params); - $request = $this->setRequestBody($request); - - // Initialize the RouteCollection - if (! $routes = $this->routes) { - $routes = Services::routes()->loadRoutes(); - } - - $routes->setHTTPVerb($method); - - // Make sure any other classes that might call the request - // instance get the right one. - Services::injectMock('request', $request); - - // Make sure filters are reset between tests - Services::injectMock('filters', Services::filters(null, false)); - - $response = $this->app - ->setContext('web') - ->setRequest($request) - ->run($routes, true); - - $output = \ob_get_contents(); - if (($response->getBody() === null) && ! ($output === '' || $output === false)) { - $response->setBody($output); - } - - // Reset directory if it has been set - Services::router()->setDirectory(null); - - // Ensure the output buffer is identical so no tests are risky - while (\ob_get_level() > $buffer) { - \ob_end_clean(); // @codeCoverageIgnore - } - - while (\ob_get_level() < $buffer) { - \ob_start(); // @codeCoverageIgnore - } - - return new FeatureResponse($response); - } - - /** - * Performs a GET request. - * - * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException - */ - public function get(string $path, ?array $params = null) - { - return $this->call('get', $path, $params); - } - - /** - * Performs a POST request. - * - * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException - */ - public function post(string $path, ?array $params = null) - { - return $this->call('post', $path, $params); - } - - /** - * Performs a PUT request - * - * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException - */ - public function put(string $path, ?array $params = null) - { - return $this->call('put', $path, $params); - } - - /** - * Performss a PATCH request - * - * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException - */ - public function patch(string $path, ?array $params = null) - { - return $this->call('patch', $path, $params); - } - - /** - * Performs a DELETE request. - * - * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException - */ - public function delete(string $path, ?array $params = null) - { - return $this->call('delete', $path, $params); - } - - /** - * Performs an OPTIONS request. - * - * @return FeatureResponse - * - * @throws Exception - * @throws RedirectException - */ - public function options(string $path, ?array $params = null) - { - return $this->call('options', $path, $params); - } - - /** - * Setup a Request object to use so that CodeIgniter - * won't try to auto-populate some of the items. - */ - protected function setupRequest(string $method, ?string $path = null): IncomingRequest - { - $config = config(App::class); - $uri = new URI(rtrim($config->baseURL, '/') . '/' . trim($path, '/ ')); - - $request = new IncomingRequest($config, clone $uri, null, new UserAgent()); - $request->uri = $uri; - - $request->setMethod($method); - $request->setProtocolVersion('1.1'); - - if ($config->forceGlobalSecureRequests) { - $_SERVER['HTTPS'] = 'test'; - } - - return $request; - } - - /** - * Setup the custom request's headers - * - * @return IncomingRequest - */ - protected function setupHeaders(IncomingRequest $request) - { - foreach ($this->headers as $name => $value) { - $request->setHeader($name, $value); - } - - return $request; - } - - /** - * Populates the data of our Request with "global" data - * relevant to the request, like $_POST data. - * - * Always populate the GET vars based on the URI. - * - * @param CLIRequest|IncomingRequest $request - * - * @return CLIRequest|IncomingRequest - * - * @throws ReflectionException - */ - protected function populateGlobals(string $method, $request, ?array $params = null) - { - // $params should set the query vars if present, - // otherwise set it from the URL. - $get = ($params !== null && $params !== [] && $method === 'get') - ? $params - : $this->getPrivateProperty($request->getUri(), 'query'); - - $request->setGlobal('get', $get); - if ($method !== 'get') { - $request->setGlobal($method, $params); - } - - $request->setGlobal('request', $params); - - $_SESSION = $this->session ?? []; - - return $request; - } - - /** - * Set the request's body formatted according to the value in $this->bodyFormat. - * 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 CLIRequest|IncomingRequest $request - * @param array|null $params The parameters to be formatted and put in the body. If this is empty, it will get the - * what has been loaded into the request global of the request class. - * - * @return CLIRequest|IncomingRequest - */ - protected function setRequestBody($request, ?array $params = null) - { - if (isset($this->requestBody) && $this->requestBody !== '') { - $request->setBody($this->requestBody); - - return $request; - } - - if (isset($this->bodyFormat) && $this->bodyFormat !== '') { - if ($params === null || $params === []) { - $params = $request->fetchGlobal('request'); - } - - $formatMime = ''; - - if ($this->bodyFormat === 'json') { - $formatMime = 'application/json'; - } elseif ($this->bodyFormat === 'xml') { - $formatMime = 'application/xml'; - } - - if ($formatMime !== '' && ! ($params === null || $params === [])) { - $formatted = Services::format()->getFormatter($formatMime)->format($params); - $request->setBody($formatted); - $request->setHeader('Content-Type', $formatMime); - } - } - - return $request; - } -} diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index 565e4e892780..0f2467ffd897 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -1,5 +1,7 @@ resetRoutes(); foreach ($routes as $route) { + if ($route[0] === strtolower($route[0])) { + @trigger_error( + 'Passing lowercase HTTP method "' . $route[0] . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($route[0]) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @TODO For backward compatibility. Remove strtolower() in the future. + * @deprecated 4.5.0 + */ + $method = strtolower($route[0]); + if (isset($route[3])) { - $collection->{$route[0]}($route[1], $route[2], $route[3]); + $collection->{$method}($route[1], $route[2], $route[3]); } else { - $collection->{$route[0]}($route[1], $route[2]); + $collection->{$method}($route[1], $route[2]); } } } @@ -147,18 +164,33 @@ public function skipEvents() */ public function call(string $method, string $path, ?array $params = null) { + if ($method === strtolower($method)) { + @trigger_error( + 'Passing lowercase HTTP method "' . $method . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($method) . '".', + E_USER_DEPRECATED + ); + } + + /** + * @deprecated 4.5.0 + * @TODO remove this in the future. + */ + $method = strtoupper($method); + // Simulate having a blank session $_SESSION = []; $_SERVER['REQUEST_METHOD'] = $method; $request = $this->setupRequest($method, $path); $request = $this->setupHeaders($request); - $request = $this->populateGlobals($method, $request, $params); + $name = strtolower($method); + $request = $this->populateGlobals($name, $request, $params); $request = $this->setRequestBody($request, $params); // Initialize the RouteCollection if (! $routes = $this->routes) { - $routes = Services::routes()->loadRoutes(); + $routes = service('routes')->loadRoutes(); } $routes->setHTTPVerb($method); @@ -179,7 +211,7 @@ public function call(string $method, string $path, ?array $params = null) ->run($routes, true); // Reset directory if it has been set - Services::router()->setDirectory(null); + service('router')->setDirectory(null); return new TestResponse($response); } @@ -196,7 +228,7 @@ public function call(string $method, string $path, ?array $params = null) */ public function get(string $path, ?array $params = null) { - return $this->call('get', $path, $params); + return $this->call(Method::GET, $path, $params); } /** @@ -209,7 +241,7 @@ public function get(string $path, ?array $params = null) */ public function post(string $path, ?array $params = null) { - return $this->call('post', $path, $params); + return $this->call(Method::POST, $path, $params); } /** @@ -222,7 +254,7 @@ public function post(string $path, ?array $params = null) */ public function put(string $path, ?array $params = null) { - return $this->call('put', $path, $params); + return $this->call(Method::PUT, $path, $params); } /** @@ -235,7 +267,7 @@ public function put(string $path, ?array $params = null) */ public function patch(string $path, ?array $params = null) { - return $this->call('patch', $path, $params); + return $this->call(Method::PATCH, $path, $params); } /** @@ -248,7 +280,7 @@ public function patch(string $path, ?array $params = null) */ public function delete(string $path, ?array $params = null) { - return $this->call('delete', $path, $params); + return $this->call(Method::DELETE, $path, $params); } /** @@ -261,7 +293,7 @@ public function delete(string $path, ?array $params = null) */ public function options(string $path, ?array $params = null) { - return $this->call('options', $path, $params); + return $this->call(Method::OPTIONS, $path, $params); } /** @@ -281,7 +313,7 @@ protected function setupRequest(string $method, ?string $path = null): IncomingR $path = $parts[0]; $query = $parts[1] ?? ''; - $superglobals = Services::superglobals(); + $superglobals = service('superglobals'); $superglobals->setServer('QUERY_STRING', $query); $uri->setPath($path); @@ -326,29 +358,29 @@ protected function setupHeaders(IncomingRequest $request) * * Always populate the GET vars based on the URI. * - * @param string $method HTTP verb + * @param string $name Superglobal name (lowercase) * @param non-empty-array|null $params * * @return Request * * @throws ReflectionException */ - protected function populateGlobals(string $method, Request $request, ?array $params = null) + protected function populateGlobals(string $name, Request $request, ?array $params = null) { // $params should set the query vars if present, // otherwise set it from the URL. - $get = ($params !== null && $params !== [] && $method === 'get') + $get = ($params !== null && $params !== [] && $name === 'get') ? $params : $this->getPrivateProperty($request->getUri(), 'query'); $request->setGlobal('get', $get); - if ($method === 'get') { + if ($name === 'get') { $request->setGlobal('request', $request->fetchGlobal('get')); } - if ($method === 'post') { - $request->setGlobal($method, $params); + if ($name === 'post') { + $request->setGlobal($name, $params); $request->setGlobal( 'request', $request->fetchGlobal('post') + $request->fetchGlobal('get') @@ -386,7 +418,7 @@ protected function setRequestBody(Request $request, ?array $params = null): Requ } if ($params !== null && $formatMime !== '') { - $formatted = Services::format()->getFormatter($formatMime)->format($params); + $formatted = service('format')->getFormatter($formatMime)->format($params); // "withBodyFormat() and $params of call()" has higher priority than withBody(). $request->setBody($formatted); } diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index 9b511e6a1d4b..93f273a39f1f 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -1,5 +1,7 @@ request ??= clone Services::request(); - $this->response ??= clone Services::response(); + $this->request ??= clone service('request'); + $this->response ??= clone service('response'); // Create our config and Filters instance to reuse for performance $this->filtersConfig ??= config(FiltersConfig::class); $this->filters ??= new Filters($this->filtersConfig, $this->request, $this->response); if ($this->collection === null) { - $this->collection = Services::routes()->loadRoutes(); + $this->collection = service('routes')->loadRoutes(); } $this->doneFilterSetUp = true; @@ -131,7 +132,7 @@ protected function getFilterCaller($filter, string $position): Closure if (is_string($filter)) { // Check for an alias (no namespace) - if (strpos($filter, '\\') === false) { + if (! str_contains($filter, '\\')) { if (! isset($this->filtersConfig->aliases[$filter])) { throw new RuntimeException("No filter found with alias '{$filter}'"); } @@ -149,7 +150,7 @@ protected function getFilterCaller($filter, string $position): Closure $filter = new $class(); if (! $filter instanceof FilterInterface) { - throw FilterException::forIncorrectInterface(get_class($filter)); + throw FilterException::forIncorrectInterface($filter::class); } $filterInstances[] = $filter; diff --git a/system/Test/Filters/CITestStreamFilter.php b/system/Test/Filters/CITestStreamFilter.php index edcd4dbfd7d6..64c3d64d65c5 100644 --- a/system/Test/Filters/CITestStreamFilter.php +++ b/system/Test/Filters/CITestStreamFilter.php @@ -1,5 +1,7 @@ setDuration($startTime); // resultID is not false, so it must be successful - if ($query->isWriteType()) { + if ($query->isWriteType($sql)) { return true; } diff --git a/system/Test/Mock/MockEmail.php b/system/Test/Mock/MockEmail.php index 3683ee8885e6..222e77007c22 100644 --- a/system/Test/Mock/MockEmail.php +++ b/system/Test/Mock/MockEmail.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Test\Mock; + +use CodeIgniter\CLI\InputOutput; +use CodeIgniter\Test\Filters\CITestStreamFilter; +use CodeIgniter\Test\PhpStreamWrapper; +use InvalidArgumentException; +use LogicException; + +final class MockInputOutput extends InputOutput +{ + /** + * String to be entered by the user. + * + * @var list + */ + private array $inputs = []; + + /** + * Output lines. + * + * @var array + * @phpstan-var list + */ + private array $outputs = []; + + /** + * Sets user inputs. + * + * @param array $inputs + * @phpstan-param list $inputs + */ + public function setInputs(array $inputs): void + { + $this->inputs = $inputs; + } + + /** + * Gets the item from the output array. + * + * @param int|null $index The output array index. If null, returns all output + * string. If negative int, returns the last $index-th + * item. + */ + public function getOutput(?int $index = null): string + { + if ($index === null) { + return implode('', $this->outputs); + } + + if (array_key_exists($index, $this->outputs)) { + return $this->outputs[$index]; + } + + if ($index < 0) { + $i = count($this->outputs) + $index; + + if (array_key_exists($i, $this->outputs)) { + return $this->outputs[$i]; + } + } + + throw new InvalidArgumentException( + 'No such index in output: ' . $index . ', the last index is: ' + . (count($this->outputs) - 1) + ); + } + + /** + * Returns the outputs array. + */ + public function getOutputs(): array + { + return $this->outputs; + } + + private function addStreamFilters(): void + { + CITestStreamFilter::registration(); + CITestStreamFilter::addOutputFilter(); + CITestStreamFilter::addErrorFilter(); + } + + private function removeStreamFilters(): void + { + CITestStreamFilter::removeOutputFilter(); + CITestStreamFilter::removeErrorFilter(); + } + + public function input(?string $prefix = null): string + { + if ($this->inputs === []) { + throw new LogicException( + 'No input data. Specifiy input data with `MockInputOutput::setInputs()`.' + ); + } + + $input = array_shift($this->inputs); + + $this->addStreamFilters(); + + PhpStreamWrapper::register(); + PhpStreamWrapper::setContent($input); + + $userInput = parent::input($prefix); + $this->outputs[] = CITestStreamFilter::$buffer . $input . PHP_EOL; + + PhpStreamWrapper::restore(); + + $this->removeStreamFilters(); + + if ($input !== $userInput) { + throw new LogicException($input . '!==' . $userInput); + } + + return $input; + } + + public function fwrite($handle, string $string): void + { + $this->addStreamFilters(); + + parent::fwrite($handle, $string); + $this->outputs[] = CITestStreamFilter::$buffer; + + $this->removeStreamFilters(); + } +} diff --git a/system/Test/Mock/MockLanguage.php b/system/Test/Mock/MockLanguage.php index 267114e2a824..80143d555f19 100644 --- a/system/Test/Mock/MockLanguage.php +++ b/system/Test/Mock/MockLanguage.php @@ -1,5 +1,7 @@ - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Test\Mock; - -use Config\Security; - -/** - * @deprecated - * - * @codeCoverageIgnore - */ -class MockSecurityConfig extends Security -{ - public string $tokenName = 'csrf_test_name'; - public string $headerName = 'X-CSRF-TOKEN'; - public string $cookieName = 'csrf_cookie_name'; - public int $expires = 7200; - public bool $regenerate = true; - public bool $redirect = false; - public string $samesite = 'Lax'; - public $excludeURIs = ['http://example.com']; -} diff --git a/system/Test/Mock/MockServices.php b/system/Test/Mock/MockServices.php index f18700426811..5d1076a6dffb 100644 --- a/system/Test/Mock/MockServices.php +++ b/system/Test/Mock/MockServices.php @@ -1,5 +1,7 @@ domParser = new DOMParser(); $body = $response->getBody(); + if (is_string($body) && $body !== '') { $this->domParser->withString($body); } @@ -144,32 +140,32 @@ public function isOK(): bool /** * Asserts that the status is a specific value. - * - * @throws Exception */ - public function assertStatus(int $code) + public function assertStatus(int $code): void { - $this->assertSame($code, $this->response->getStatusCode()); + Assert::assertSame($code, $this->response->getStatusCode()); } /** * Asserts that the Response is considered OK. - * - * @throws Exception */ - public function assertOK() + public function assertOK(): void { - $this->assertTrue($this->isOK(), "{$this->response->getStatusCode()} is not a successful status code, or the Response has an empty body."); + Assert::assertTrue( + $this->isOK(), + "{$this->response->getStatusCode()} is not a successful status code, or Response has an empty body." + ); } /** * Asserts that the Response is considered not OK. - * - * @throws Exception */ - public function assertNotOK() + public function assertNotOK(): void { - $this->assertFalse($this->isOK(), "{$this->response->getStatusCode()} is an unexpected successful status code, or the Response has body content."); + Assert::assertFalse( + $this->isOK(), + "{$this->response->getStatusCode()} is an unexpected successful status code, or Response body has content." + ); } // -------------------------------------------------------------------- @@ -188,21 +184,17 @@ public function isRedirect(): bool /** * Assert that the given response was a redirect. - * - * @throws Exception */ - public function assertRedirect() + public function assertRedirect(): void { - $this->assertTrue($this->isRedirect(), 'Response is not a redirect or RedirectResponse.'); + Assert::assertTrue($this->isRedirect(), 'Response is not a redirect or instance of RedirectResponse.'); } /** * Assert that a given response was a redirect * and it was redirect to a specific URI. - * - * @throws Exception */ - public function assertRedirectTo(string $uri) + public function assertRedirectTo(string $uri): void { $this->assertRedirect(); @@ -210,20 +202,18 @@ public function assertRedirectTo(string $uri) $redirectUri = strtolower($this->getRedirectUrl()); $matches = $uri === $redirectUri - || strtolower(site_url($uri)) === $redirectUri - || $uri === site_url($redirectUri); + || strtolower(site_url($uri)) === $redirectUri + || $uri === site_url($redirectUri); - $this->assertTrue($matches, "Redirect URL `{$uri}` does not match `{$redirectUri}`"); + Assert::assertTrue($matches, "Redirect URL '{$uri}' does not match '{$redirectUri}'."); } /** * Assert that the given response was not a redirect. - * - * @throws Exception */ - public function assertNotRedirect() + public function assertNotRedirect(): void { - $this->assertFalse($this->isRedirect(), 'Response is an unexpected redirect or RedirectResponse.'); + Assert::assertFalse($this->isRedirect(), 'Response is an unexpected redirect or instance of RedirectResponse.'); } /** @@ -251,35 +241,33 @@ public function getRedirectUrl(): ?string // -------------------------------------------------------------------- /** - * Asserts that an SESSION key has been set and, optionally, test it's value. + * Asserts that an SESSION key has been set and, optionally, test its value. * * @param mixed $value - * - * @throws Exception */ - public function assertSessionHas(string $key, $value = null) + public function assertSessionHas(string $key, $value = null): void { - $this->assertArrayHasKey($key, $_SESSION, "'{$key}' is not in the current \$_SESSION"); + Assert::assertArrayHasKey($key, $_SESSION, "Key '{$key}' is not in the current \$_SESSION"); if ($value === null) { return; } if (is_scalar($value)) { - $this->assertSame($value, $_SESSION[$key], "The value of '{$key}' ({$value}) does not match expected value."); - } else { - $this->assertSame($value, $_SESSION[$key], "The value of '{$key}' does not match expected value."); + Assert::assertSame($value, $_SESSION[$key], "The value of key '{$key}' ({$value}) does not match expected value."); + + return; } + + Assert::assertSame($value, $_SESSION[$key], "The value of key '{$key}' does not match expected value."); } /** * Asserts the session is missing $key. - * - * @throws Exception */ - public function assertSessionMissing(string $key) + public function assertSessionMissing(string $key): void { - $this->assertArrayNotHasKey($key, $_SESSION, "'{$key}' should not be present in \$_SESSION."); + Assert::assertArrayNotHasKey($key, $_SESSION, "Key '{$key}' should not be present in \$_SESSION."); } // -------------------------------------------------------------------- @@ -290,26 +278,26 @@ public function assertSessionMissing(string $key) * Asserts that the Response contains a specific header. * * @param string|null $value - * - * @throws Exception */ - public function assertHeader(string $key, $value = null) + public function assertHeader(string $key, $value = null): void { - $this->assertTrue($this->response->hasHeader($key), "'{$key}' is not a valid Response header."); + Assert::assertTrue($this->response->hasHeader($key), "Header '{$key}' is not a valid Response header."); if ($value !== null) { - $this->assertSame($value, $this->response->getHeaderLine($key), "The value of '{$key}' header ({$this->response->getHeaderLine($key)}) does not match expected value."); + Assert::assertSame( + $value, + $this->response->getHeaderLine($key), + "The value of '{$key}' header ({$this->response->getHeaderLine($key)}) does not match expected value." + ); } } /** * Asserts the Response headers does not contain the specified header. - * - * @throws Exception */ - public function assertHeaderMissing(string $key) + public function assertHeaderMissing(string $key): void { - $this->assertFalse($this->response->hasHeader($key), "'{$key}' should not be in the Response headers."); + Assert::assertFalse($this->response->hasHeader($key), "Header '{$key}' should not be in the Response headers."); } // -------------------------------------------------------------------- @@ -320,31 +308,31 @@ public function assertHeaderMissing(string $key) * Asserts that the response has the specified cookie. * * @param string|null $value - * - * @throws Exception */ - public function assertCookie(string $key, $value = null, string $prefix = '') + public function assertCookie(string $key, $value = null, string $prefix = ''): void { - $this->assertTrue($this->response->hasCookie($key, $value, $prefix), "No cookie found named '{$key}'."); + Assert::assertTrue($this->response->hasCookie($key, $value, $prefix), "Cookie named '{$key}' is not found."); } /** * Assert the Response does not have the specified cookie set. */ - public function assertCookieMissing(string $key) + public function assertCookieMissing(string $key): void { - $this->assertFalse($this->response->hasCookie($key), "Cookie named '{$key}' should not be set."); + Assert::assertFalse($this->response->hasCookie($key), "Cookie named '{$key}' should not be set."); } /** * Asserts that a cookie exists and has an expired time. - * - * @throws Exception */ - public function assertCookieExpired(string $key, string $prefix = '') + public function assertCookieExpired(string $key, string $prefix = ''): void { - $this->assertTrue($this->response->hasCookie($key, null, $prefix)); - $this->assertGreaterThan(Time::now()->getTimestamp(), $this->response->getCookie($key, $prefix)->getExpiresTimestamp()); + Assert::assertTrue($this->response->hasCookie($key, null, $prefix)); + + Assert::assertGreaterThan( + Time::now()->getTimestamp(), + $this->response->getCookie($key, $prefix)->getExpiresTimestamp() + ); } // -------------------------------------------------------------------- @@ -369,20 +357,21 @@ public function getJSON() /** * Test that the response contains a matching JSON fragment. - * - * @throws Exception */ - public function assertJSONFragment(array $fragment, bool $strict = false) + public function assertJSONFragment(array $fragment, bool $strict = false): void { $json = json_decode($this->getJSON(), true); - $this->assertIsArray($json, 'Response does not have valid json'); + Assert::assertIsArray($json, 'Response is not a valid JSON.'); + $patched = array_replace_recursive($json, $fragment); if ($strict) { - $this->assertSame($json, $patched, 'Response does not contain a matching JSON fragment.'); - } else { - $this->assertThat($patched, new IsEqual($json), 'Response does not contain a matching JSON fragment.'); + Assert::assertSame($json, $patched, 'Response does not contain a matching JSON fragment.'); + + return; } + + Assert::assertThat($patched, new IsEqual($json), 'Response does not contain a matching JSON fragment.'); } /** @@ -390,10 +379,8 @@ public function assertJSONFragment(array $fragment, bool $strict = false) * If the value being passed in is a string, it must be a json_encoded string. * * @param array|object|string $test - * - * @throws Exception */ - public function assertJSONExact($test) + public function assertJSONExact($test): void { $json = $this->getJSON(); @@ -402,10 +389,10 @@ public function assertJSONExact($test) } if (is_array($test)) { - $test = Services::format()->getFormatter('application/json')->format($test); + $test = service('format')->getFormatter('application/json')->format($test); } - $this->assertJsonStringEqualsJsonString($test, $json, 'Response does not contain matching JSON.'); + Assert::assertJsonStringEqualsJsonString($test, $json, 'Response does not contain matching JSON.'); } // -------------------------------------------------------------------- @@ -415,7 +402,7 @@ public function assertJSONExact($test) /** * Returns the response' body as XML * - * @return mixed|string + * @return bool|string|null */ public function getXML() { @@ -428,76 +415,81 @@ public function getXML() /** * Assert that the desired text can be found in the result body. - * - * @throws Exception */ - public function assertSee(?string $search = null, ?string $element = null) + public function assertSee(?string $search = null, ?string $element = null): void { - $this->assertTrue($this->domParser->see($search, $element), "Do not see '{$search}' in response."); + Assert::assertTrue( + $this->domParser->see($search, $element), + "Text '{$search}' is not seen in response." + ); } /** * Asserts that we do not see the specified text. - * - * @throws Exception */ - public function assertDontSee(?string $search = null, ?string $element = null) + public function assertDontSee(?string $search = null, ?string $element = null): void { - $this->assertTrue($this->domParser->dontSee($search, $element), "I should not see '{$search}' in response."); + Assert::assertTrue( + $this->domParser->dontSee($search, $element), + "Text '{$search}' is unexpectedly seen in response." + ); } /** * Assert that we see an element selected via a CSS selector. - * - * @throws Exception */ - public function assertSeeElement(string $search) + public function assertSeeElement(string $search): void { - $this->assertTrue($this->domParser->seeElement($search), "Do not see element with selector '{$search} in response.'"); + Assert::assertTrue( + $this->domParser->seeElement($search), + "Element with selector '{$search}' is not seen in response." + ); } /** * Assert that we do not see an element selected via a CSS selector. - * - * @throws Exception */ - public function assertDontSeeElement(string $search) + public function assertDontSeeElement(string $search): void { - $this->assertTrue($this->domParser->dontSeeElement($search), "I should not see an element with selector '{$search}' in response.'"); + Assert::assertTrue( + $this->domParser->dontSeeElement($search), + "Element with selector '{$search}' is unexpectedly seen in response.'" + ); } /** * Assert that we see a link with the matching text and/or class. - * - * @throws Exception */ - public function assertSeeLink(string $text, ?string $details = null) + public function assertSeeLink(string $text, ?string $details = null): void { - $this->assertTrue($this->domParser->seeLink($text, $details), "Do no see anchor tag with the text {$text} in response."); + Assert::assertTrue( + $this->domParser->seeLink($text, $details), + "Anchor tag with text '{$text}' is not seen in response." + ); } /** * Assert that we see an input with name/value. - * - * @throws Exception */ - public function assertSeeInField(string $field, ?string $value = null) + public function assertSeeInField(string $field, ?string $value = null): void { - $this->assertTrue($this->domParser->seeInField($field, $value), "Do no see input named {$field} with value {$value} in response."); + Assert::assertTrue( + $this->domParser->seeInField($field, $value), + "Input named '{$field}' with value '{$value}' is not seen in response." + ); } /** * Forward any unrecognized method calls to our DOMParser instance. * - * @param string $function Method name - * @param mixed $params Any method parameters - * - * @return mixed + * @param list $params */ - public function __call($function, $params) + public function __call(string $function, array $params): mixed { if (method_exists($this->domParser, $function)) { return $this->domParser->{$function}(...$params); } + + return null; } } diff --git a/system/Test/bootstrap.php b/system/Test/bootstrap.php index 57196190f527..4a068a4c97b1 100644 --- a/system/Test/bootstrap.php +++ b/system/Test/bootstrap.php @@ -1,5 +1,7 @@ appDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); -defined('WRITEPATH') || define('WRITEPATH', realpath(rtrim($paths->writableDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); -defined('SYSTEMPATH') || define('SYSTEMPATH', realpath(rtrim($paths->systemDirectory, '\\/')) . DIRECTORY_SEPARATOR); -defined('ROOTPATH') || define('ROOTPATH', realpath(APPPATH . '../') . DIRECTORY_SEPARATOR); -defined('CIPATH') || define('CIPATH', realpath(SYSTEMPATH . '../') . DIRECTORY_SEPARATOR); -defined('FCPATH') || define('FCPATH', realpath(PUBLICPATH) . DIRECTORY_SEPARATOR); -defined('TESTPATH') || define('TESTPATH', realpath(HOMEPATH . 'tests/') . DIRECTORY_SEPARATOR); +defined('APPPATH') || define('APPPATH', realpath(rtrim($paths->appDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); +defined('ROOTPATH') || define('ROOTPATH', realpath(APPPATH . '../') . DIRECTORY_SEPARATOR); +defined('SYSTEMPATH') || define('SYSTEMPATH', realpath(rtrim($paths->systemDirectory, '\\/')) . DIRECTORY_SEPARATOR); +defined('WRITEPATH') || define('WRITEPATH', realpath(rtrim($paths->writableDirectory, '\\/ ')) . DIRECTORY_SEPARATOR); +defined('TESTPATH') || define('TESTPATH', realpath(HOMEPATH . 'tests/') . DIRECTORY_SEPARATOR); + +defined('CIPATH') || define('CIPATH', realpath(SYSTEMPATH . '../') . DIRECTORY_SEPARATOR); +defined('FCPATH') || define('FCPATH', realpath(PUBLICPATH) . DIRECTORY_SEPARATOR); + defined('SUPPORTPATH') || define('SUPPORTPATH', realpath(TESTPATH . '_support/') . DIRECTORY_SEPARATOR); defined('COMPOSER_PATH') || define('COMPOSER_PATH', (string) realpath(HOMEPATH . 'vendor/autoload.php')); defined('VENDORPATH') || define('VENDORPATH', realpath(HOMEPATH . 'vendor') . DIRECTORY_SEPARATOR); -// Load Common.php from App then System -if (is_file(APPPATH . 'Common.php')) { - require_once APPPATH . 'Common.php'; -} - -require_once SYSTEMPATH . 'Common.php'; - -// Set environment values that would otherwise stop the framework from functioning during tests. -if (! isset($_SERVER['app.baseURL'])) { - $_SERVER['app.baseURL'] = 'http://example.com/'; -} - -// Load necessary components -require_once SYSTEMPATH . 'Config/AutoloadConfig.php'; -require_once APPPATH . 'Config/Autoload.php'; -require_once APPPATH . 'Config/Constants.php'; -require_once SYSTEMPATH . 'Modules/Modules.php'; -require_once APPPATH . 'Config/Modules.php'; - -require_once SYSTEMPATH . 'Autoloader/Autoloader.php'; -require_once SYSTEMPATH . 'Config/BaseService.php'; -require_once SYSTEMPATH . 'Config/Services.php'; -require_once APPPATH . 'Config/Services.php'; - -// Initialize and register the loader with the SPL autoloader stack. -Services::autoloader()->initialize(new Autoload(), new Modules())->register(); -Services::autoloader()->loadHelpers(); - -// Now load Composer's if it's available -if (is_file(COMPOSER_PATH)) { - require_once COMPOSER_PATH; -} +/* + *--------------------------------------------------------------- + * BOOTSTRAP THE APPLICATION + *--------------------------------------------------------------- + * This process sets up the path constants, loads and registers + * our autoloader, along with Composer's, loads our constants + * and fires up an environment-specific bootstrapping. + */ -// Load environment settings from .env files into $_SERVER and $_ENV -require_once SYSTEMPATH . 'Config/DotEnv.php'; +// LOAD THE FRAMEWORK BOOTSTRAP FILE +require $paths->systemDirectory . '/Boot.php'; +Boot::bootTest($paths); -$env = new DotEnv(ROOTPATH); -$env->load(); +/* + * --------------------------------------------------------------- + * LOAD ROUTES + * --------------------------------------------------------------- + */ Services::routes()->loadRoutes(); diff --git a/system/ThirdParty/Escaper/Escaper.php b/system/ThirdParty/Escaper/Escaper.php index d6a02e14c9be..c4964cb55e4a 100644 --- a/system/ThirdParty/Escaper/Escaper.php +++ b/system/ThirdParty/Escaper/Escaper.php @@ -157,9 +157,21 @@ public function __construct(?string $encoding = null) $this->htmlSpecialCharsFlags = ENT_QUOTES | ENT_SUBSTITUTE; // set matcher callbacks - $this->htmlAttrMatcher = [$this, 'htmlAttrMatcher']; - $this->jsMatcher = [$this, 'jsMatcher']; - $this->cssMatcher = [$this, 'cssMatcher']; + $this->htmlAttrMatcher = + /** @param array $matches */ + function (array $matches): string { + return $this->htmlAttrMatcher($matches); + }; + $this->jsMatcher = + /** @param array $matches */ + function (array $matches): string { + return $this->jsMatcher($matches); + }; + $this->cssMatcher = + /** @param array $matches */ + function (array $matches): string { + return $this->cssMatcher($matches); + }; } /** diff --git a/system/ThirdParty/PSR/Log/AbstractLogger.php b/system/ThirdParty/PSR/Log/AbstractLogger.php index e02f9daf3d51..d60a091affae 100644 --- a/system/ThirdParty/PSR/Log/AbstractLogger.php +++ b/system/ThirdParty/PSR/Log/AbstractLogger.php @@ -11,118 +11,5 @@ */ abstract class AbstractLogger implements LoggerInterface { - /** - * System is unusable. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function emergency($message, array $context = array()) - { - $this->log(LogLevel::EMERGENCY, $message, $context); - } - - /** - * Action must be taken immediately. - * - * Example: Entire website down, database unavailable, etc. This should - * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function alert($message, array $context = array()) - { - $this->log(LogLevel::ALERT, $message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function critical($message, array $context = array()) - { - $this->log(LogLevel::CRITICAL, $message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function error($message, array $context = array()) - { - $this->log(LogLevel::ERROR, $message, $context); - } - - /** - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things - * that are not necessarily wrong. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function warning($message, array $context = array()) - { - $this->log(LogLevel::WARNING, $message, $context); - } - - /** - * Normal but significant events. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function notice($message, array $context = array()) - { - $this->log(LogLevel::NOTICE, $message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function info($message, array $context = array()) - { - $this->log(LogLevel::INFO, $message, $context); - } - - /** - * Detailed debug information. - * - * @param string $message - * @param mixed[] $context - * - * @return void - */ - public function debug($message, array $context = array()) - { - $this->log(LogLevel::DEBUG, $message, $context); - } + use LoggerTrait; } diff --git a/system/ThirdParty/PSR/Log/LoggerAwareInterface.php b/system/ThirdParty/PSR/Log/LoggerAwareInterface.php index 4d64f4786adb..cc46a95147cd 100644 --- a/system/ThirdParty/PSR/Log/LoggerAwareInterface.php +++ b/system/ThirdParty/PSR/Log/LoggerAwareInterface.php @@ -14,5 +14,5 @@ interface LoggerAwareInterface * * @return void */ - public function setLogger(LoggerInterface $logger); + public function setLogger(LoggerInterface $logger): void; } diff --git a/system/ThirdParty/PSR/Log/LoggerAwareTrait.php b/system/ThirdParty/PSR/Log/LoggerAwareTrait.php index 82bf45c89bab..4fb57a292263 100644 --- a/system/ThirdParty/PSR/Log/LoggerAwareTrait.php +++ b/system/ThirdParty/PSR/Log/LoggerAwareTrait.php @@ -12,14 +12,14 @@ trait LoggerAwareTrait * * @var LoggerInterface|null */ - protected $logger; + protected ?LoggerInterface $logger = null; /** * Sets a logger. * * @param LoggerInterface $logger */ - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } diff --git a/system/ThirdParty/PSR/Log/LoggerInterface.php b/system/ThirdParty/PSR/Log/LoggerInterface.php index 2206cfde41ae..b3a24b5f7e9d 100644 --- a/system/ThirdParty/PSR/Log/LoggerInterface.php +++ b/system/ThirdParty/PSR/Log/LoggerInterface.php @@ -22,12 +22,12 @@ interface LoggerInterface /** * System is unusable. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function emergency($message, array $context = array()); + public function emergency(string|\Stringable $message, array $context = []): void; /** * Action must be taken immediately. @@ -35,35 +35,35 @@ public function emergency($message, array $context = array()); * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function alert($message, array $context = array()); + public function alert(string|\Stringable $message, array $context = []): void; /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function critical($message, array $context = array()); + public function critical(string|\Stringable $message, array $context = []): void; /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function error($message, array $context = array()); + public function error(string|\Stringable $message, array $context = []): void; /** * Exceptional occurrences that are not errors. @@ -71,55 +71,55 @@ public function error($message, array $context = array()); * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function warning($message, array $context = array()); + public function warning(string|\Stringable $message, array $context = []): void; /** * Normal but significant events. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function notice($message, array $context = array()); + public function notice(string|\Stringable $message, array $context = []): void; /** * Interesting events. * * Example: User logs in, SQL logs. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function info($message, array $context = array()); + public function info(string|\Stringable $message, array $context = []): void; /** * Detailed debug information. * - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void */ - public function debug($message, array $context = array()); + public function debug(string|\Stringable $message, array $context = []): void; /** * Logs with an arbitrary level. * * @param mixed $level - * @param string $message + * @param string|\Stringable $message * @param mixed[] $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ - public function log($level, $message, array $context = array()); + public function log($level, string|\Stringable $message, array $context = []): void; } diff --git a/system/ThirdParty/PSR/Log/LoggerTrait.php b/system/ThirdParty/PSR/Log/LoggerTrait.php index e392fef0a0cb..9c8733f957fd 100644 --- a/system/ThirdParty/PSR/Log/LoggerTrait.php +++ b/system/ThirdParty/PSR/Log/LoggerTrait.php @@ -15,12 +15,12 @@ trait LoggerTrait /** * System is unusable. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function emergency($message, array $context = array()) + public function emergency(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } @@ -31,12 +31,12 @@ public function emergency($message, array $context = array()) * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function alert($message, array $context = array()) + public function alert(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } @@ -46,12 +46,12 @@ public function alert($message, array $context = array()) * * Example: Application component unavailable, unexpected exception. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function critical($message, array $context = array()) + public function critical(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } @@ -60,12 +60,12 @@ public function critical($message, array $context = array()) * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function error($message, array $context = array()) + public function error(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } @@ -76,12 +76,12 @@ public function error($message, array $context = array()) * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function warning($message, array $context = array()) + public function warning(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } @@ -89,12 +89,12 @@ public function warning($message, array $context = array()) /** * Normal but significant events. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function notice($message, array $context = array()) + public function notice(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } @@ -104,12 +104,12 @@ public function notice($message, array $context = array()) * * Example: User logs in, SQL logs. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function info($message, array $context = array()) + public function info(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } @@ -117,12 +117,12 @@ public function info($message, array $context = array()) /** * Detailed debug information. * - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void */ - public function debug($message, array $context = array()) + public function debug(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } @@ -131,12 +131,12 @@ public function debug($message, array $context = array()) * Logs with an arbitrary level. * * @param mixed $level - * @param string $message + * @param string|\Stringable $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ - abstract public function log($level, $message, array $context = array()); + abstract public function log($level, string|\Stringable $message, array $context = []): void; } diff --git a/system/ThirdParty/PSR/Log/NullLogger.php b/system/ThirdParty/PSR/Log/NullLogger.php index c8f7293b1c66..c1cc3c0692a2 100644 --- a/system/ThirdParty/PSR/Log/NullLogger.php +++ b/system/ThirdParty/PSR/Log/NullLogger.php @@ -16,14 +16,14 @@ class NullLogger extends AbstractLogger * Logs with an arbitrary level. * * @param mixed $level - * @param string $message - * @param array $context + * @param string|\Stringable $message + * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ - public function log($level, $message, array $context = array()) + public function log($level, string|\Stringable $message, array $context = []): void { // noop } diff --git a/system/Throttle/Throttler.php b/system/Throttle/Throttler.php index 0a10c02335f2..44d12005a80d 100644 --- a/system/Throttle/Throttler.php +++ b/system/Throttle/Throttler.php @@ -1,5 +1,7 @@ )#s', $str, $matches)) { + if (str_contains($str, ' +The ID field is required. + +' . "\n", $html); } public function testValidationShowErrorForWildcards(): void @@ -1039,7 +1048,10 @@ public function testValidationShowErrorForWildcards(): void $html = validation_show_error('user.*.name'); - $this->assertSame('The Name field is required.' . "\n", $html); + $this->assertSame(' +The Name field is required. + +' . "\n", $html); } public function testFormParseFormAttributesTrue(): void diff --git a/tests/system/Helpers/HTMLHelperTest.php b/tests/system/Helpers/HTMLHelperTest.php index f591b841024d..0fc05a77ea91 100755 --- a/tests/system/Helpers/HTMLHelperTest.php +++ b/tests/system/Helpers/HTMLHelperTest.php @@ -1,5 +1,7 @@ assertSame('file_5', increment_string('file_4')); $this->assertSame('file-5', increment_string('file-4', '-')); $this->assertSame('file-5', increment_string('file-4', '-')); - $this->assertSame('file-1', increment_string('file', '-', '1')); + $this->assertSame('file-1', increment_string('file', '-', 1)); $this->assertSame('124', increment_string('123', '')); } diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index c735064fa287..ab5c5c381f49 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -1,5 +1,7 @@ group('(:alpha)', static function ($routes): void { - $routes->match(['get'], 'login', 'Common\LoginController::loginView', ['as' => 'loginURL']); + $routes->match(['GET'], 'login', 'Common\LoginController::loginView', ['as' => 'loginURL']); }); url_to('loginURL'); diff --git a/tests/system/Helpers/URLHelper/SiteUrlCliTest.php b/tests/system/Helpers/URLHelper/SiteUrlCliTest.php index c675cd326272..526d2250f2c7 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlCliTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlCliTest.php @@ -1,5 +1,7 @@ withRoutes([ [ - 'get', + 'GET', 'home', '\App\Controllers\Home::index', ], diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php index 0ed98e769f50..c243645d905b 100644 --- a/tests/system/Honeypot/HoneypotTest.php +++ b/tests/system/Honeypot/HoneypotTest.php @@ -1,5 +1,7 @@ assertTrue($this->honeypot->hasContent($this->request)); } - public function testConfigHidden(): void - { - $this->config->hidden = ''; - $this->expectException(HoneypotException::class); - $this->honeypot = new Honeypot($this->config); - } - public function testConfigTemplate(): void { $this->config->template = ''; diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index f5de5da523d5..5ec7d02f9974 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -1,5 +1,7 @@ log('foo', ''); } - public function testLogReturnsFalseWhenLogNotHandled(): void + public function testLogAlwaysReturnsVoid(): void { $config = new LoggerConfig(); $config->threshold = 3; $logger = new Logger($config); - $this->assertFalse($logger->log('debug', '')); + $this->assertNull($logger->log('debug', '')); } public function testLogActuallyLogs(): void @@ -404,16 +407,12 @@ public function testLogLevels(): void public function testNonStringMessage(): void { + $this->expectException(TypeError::class); + $config = new LoggerConfig(); $logger = new Logger($config); - $expected = '[Tests\Support\Log\Handlers\TestHandler]'; $logger->log(5, $config); - - $logs = TestHandler::getLogs(); - - $this->assertCount(1, $logs); - $this->assertStringContainsString($expected, $logs[0]); } public function testDetermineFileNoStackTrace(): void diff --git a/tests/system/Models/AffectedRowsTest.php b/tests/system/Models/AffectedRowsTest.php index 43e14c1ec638..9c7d60267ebc 100644 --- a/tests/system/Models/AffectedRowsTest.php +++ b/tests/system/Models/AffectedRowsTest.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Models; + +use CodeIgniter\I18n\Time; +use Tests\Support\Entity\CustomUser; +use Tests\Support\Entity\User; +use Tests\Support\Models\UserCastsTimestampModel; + +/** + * @group DatabaseLive + * + * @internal + */ +final class DataConverterModelTest extends LiveModelTestCase +{ + protected $migrate = true; + protected $migrateOnce = true; + protected $refresh = false; + protected $seed = ''; + + public function testFindAsArray(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->find($id); + + $this->assertIsInt($user['id']); + $this->assertInstanceOf(Time::class, $user['created_at']); + $this->assertSame('John Smith', $user['name']); + // `name` is cast by custom CastBase64 handler. + $this->seeInDatabase('user', ['name' => 'Sm9obiBTbWl0aA==']); + } + + /** + * @return int|string Insert ID + */ + private function prepareOneRecord(): int|string + { + $this->createModel(UserCastsTimestampModel::class); + $this->db->table('user')->truncate(); + + $data = [ + 'name' => 'John Smith', + 'email' => ['john@example.com'], + 'country' => 'US', + ]; + + return $this->model->insert($data, true); + } + + public function testFindAsObject(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->asObject()->find($id); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFindAsCustomObject(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->asObject(CustomUser::class)->find($id); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFindAsEntity(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->asObject(User::class)->find($id); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFindAllAsArray(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->findAll(); + + $this->assertIsInt($users[0]['id']); + $this->assertInstanceOf(Time::class, $users[0]['created_at']); + $this->assertIsInt($users[1]['id']); + $this->assertInstanceOf(Time::class, $users[1]['created_at']); + } + + private function prepareTwoRecords(): void + { + $this->prepareOneRecord(); + + $data = [ + 'name' => 'Mike Smith', + 'email' => ['mike@example.com'], + 'country' => 'CA', + ]; + $this->model->insert($data); + } + + public function testFindAllAsObject(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->asObject()->findAll(); + + $this->assertIsInt($users[0]->id); + $this->assertInstanceOf(Time::class, $users[0]->created_at); + $this->assertIsInt($users[1]->id); + $this->assertInstanceOf(Time::class, $users[1]->created_at); + } + + public function testFindAllAsCustomObject(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->asObject(CustomUser::class)->findAll(); + + $this->assertIsInt($users[0]->id); + $this->assertInstanceOf(Time::class, $users[0]->created_at); + $this->assertIsInt($users[1]->id); + $this->assertInstanceOf(Time::class, $users[1]->created_at); + } + + public function testFindAllAsEntity(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->asObject(User::class)->findAll(); + + $this->assertIsInt($users[0]->id); + $this->assertInstanceOf(Time::class, $users[0]->created_at); + $this->assertIsInt($users[1]->id); + $this->assertInstanceOf(Time::class, $users[1]->created_at); + } + + public function testFindColumn(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->findColumn('created_at'); + + $this->assertInstanceOf(Time::class, $users[0]); + $this->assertInstanceOf(Time::class, $users[1]); + } + + public function testFirstAsArray(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->first(); + + $this->assertIsInt($user['id']); + $this->assertInstanceOf(Time::class, $user['created_at']); + } + + public function testFirstAsObject(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->asObject()->first(); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFirstAsCustomObject(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->asObject(CustomUser::class)->first(); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFirstAsEntity(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->asObject(User::class)->first(); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testInsertArray(): void + { + $this->prepareOneRecord(); + + $data = [ + 'name' => 'Joe Smith', + 'email' => ['joe@example.com'], + 'country' => 'GB', + ]; + $id = $this->model->insert($data, true); + + $user = $this->model->find($id); + $this->assertSame(['joe@example.com'], $user['email']); + } + + public function testInsertObject(): void + { + $this->prepareOneRecord(); + + $data = (object) [ + 'name' => 'Joe Smith', + 'email' => ['joe@example.com'], + 'country' => 'GB', + ]; + $id = $this->model->insert($data, true); + + $user = $this->model->find($id); + $this->assertSame(['joe@example.com'], $user['email']); + } + + public function testUpdateArray(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->find($id); + + $user['email'][] = 'private@example.org'; + $this->model->update($user['id'], $user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testUpdateObject(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->asObject()->find($id); + + $user->email[] = 'private@example.org'; + $this->model->update($user->id, $user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testUpdateCustomObject(): void + { + $id = $this->prepareOneRecord(); + /** @var CustomUser $user */ + $user = $this->model->asObject(CustomUser::class)->find($id); + + $user->addEmail('private@example.org'); + $this->model->update($user->id, $user); + + $user = $this->model->asObject(CustomUser::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } + + public function testUpdateEntity(): void + { + $id = $this->prepareOneRecord(); + /** @var User $user */ + $user = $this->model->asObject(User::class)->find($id); + + $email = $user->email; + $email[] = 'private@example.org'; + $user->email = $email; + $this->model->update($user->id, $user); + + $user = $this->model->asObject(User::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } + + public function testSaveArray(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->find($id); + + $user['email'][] = 'private@example.org'; + $this->model->save($user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testSaveObject(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->asObject()->find($id); + + $user->email[] = 'private@example.org'; + $this->model->save($user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testSaveCustomObject(): void + { + $id = $this->prepareOneRecord(); + /** @var CustomUser $user */ + $user = $this->model->asObject(CustomUser::class)->find($id); + + $user->addEmail('private@example.org'); + $this->model->save($user); + + $user = $this->model->asObject(CustomUser::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } + + public function testSaveEntity(): void + { + $id = $this->prepareOneRecord(); + /** @var User $user */ + $user = $this->model->asObject(User::class)->find($id); + + $email = $user->email; + $email[] = 'private@example.org'; + $user->email = $email; + $this->model->save($user); + + $user = $this->model->asObject(User::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } +} diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index 121ff40600fe..bd2af1f4ad0d 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -1,5 +1,7 @@ createModel(UserModel::class)->delete($emptyValue); - } catch (DatabaseException $e) { + } catch (DatabaseException) { // Do nothing. } diff --git a/tests/system/Models/EventsModelTest.php b/tests/system/Models/EventsModelTest.php index 6f9ba63df6c4..52130f73f9f8 100644 --- a/tests/system/Models/EventsModelTest.php +++ b/tests/system/Models/EventsModelTest.php @@ -1,5 +1,7 @@ assertSame(2, $this->model->insertBatch($jobData)); $result = $this->model->where('name', 'Philosopher')->first(); - $this->assertCloseEnough(time(), $result->created_at); + $this->assertCloseEnough(time(), (int) $result->created_at); } public function testInsertBatchSetsDatetimeTimestamps(): void diff --git a/tests/system/Models/LiveModelTestCase.php b/tests/system/Models/LiveModelTestCase.php index de63907b903d..ad250ecc73b7 100644 --- a/tests/system/Models/LiveModelTestCase.php +++ b/tests/system/Models/LiveModelTestCase.php @@ -1,5 +1,7 @@ find(1); + $updateAtBefore = $user->updated_at; + + // updates the Entity without changes. + $result = $model->update(1, $user); + + $user = $model->find(1); + $updateAtAfter = $user->updated_at; + + $this->assertTrue($result); + $this->assertNotSame($updateAtBefore, $updateAtAfter); + } } diff --git a/tests/system/Models/ValidationModelRuleGroupTest.php b/tests/system/Models/ValidationModelRuleGroupTest.php index 0064b4f0c9ed..e1de82841832 100644 --- a/tests/system/Models/ValidationModelRuleGroupTest.php +++ b/tests/system/Models/ValidationModelRuleGroupTest.php @@ -1,5 +1,7 @@ resetServices(true); $this->resetFactories(); + + // Workaround for errors on PHPUnit 10 and PHP 8.3. + // See https://github.com/sebastianbergmann/phpunit/issues/5403#issuecomment-1906810619 + restore_error_handler(); } private function createCodeigniter(): void diff --git a/tests/system/RESTful/ResourcePresenterTest.php b/tests/system/RESTful/ResourcePresenterTest.php index affaab379255..975f0d2b9e0c 100644 --- a/tests/system/RESTful/ResourcePresenterTest.php +++ b/tests/system/RESTful/ResourcePresenterTest.php @@ -1,5 +1,7 @@ resetServices(true); $this->resetFactories(); + + // Workaround for errors on PHPUnit 10 and PHP 8.3. + // See https://github.com/sebastianbergmann/phpunit/issues/5403#issuecomment-1906810619 + restore_error_handler(); } private function createCodeigniter(): void diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 9360352f4900..1441e2ca49e9 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -1,5 +1,7 @@ collection = new RouteCollection(Services::locator(), $moduleConfig, new Routing()); } - private function createNewAutoRouter(string $httpVerb = 'get', $namespace = 'CodeIgniter\Router\Controllers'): AutoRouterImproved + private function createNewAutoRouter($namespace = 'CodeIgniter\Router\Controllers'): AutoRouterImproved { return new AutoRouterImproved( [], $namespace, $this->collection->getDefaultController(), $this->collection->getDefaultMethod(), - true, - $httpVerb + true ); } @@ -59,7 +62,7 @@ public function testAutoRouteFindsDefaultControllerAndMethodGet(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('/', 'get'); + = $router->getRoute('/', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -82,10 +85,10 @@ public function testAutoRouteFindsModuleDefaultControllerAndMethodGet() $this->collection->setDefaultController('Index'); - $router = $this->createNewAutoRouter('get', 'App/Controllers'); + $router = $this->createNewAutoRouter('App/Controllers'); [$directory, $controller, $method, $params] - = $router->getRoute('test', 'get'); + = $router->getRoute('test', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -97,10 +100,10 @@ public function testAutoRouteFindsDefaultControllerAndMethodPost(): void { $this->collection->setDefaultController('Index'); - $router = $this->createNewAutoRouter('post'); + $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('/', 'post'); + = $router->getRoute('/', Method::POST); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -113,7 +116,7 @@ public function testAutoRouteFindsControllerWithFileAndMethod(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod', 'get'); + = $router->getRoute('mycontroller/somemethod', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -131,7 +134,7 @@ public function testFindsControllerAndMethodAndParam(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod/a', 'get'); + = $router->getRoute('mycontroller/somemethod/a', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -153,7 +156,7 @@ public function testUriParamCountIsGreaterThanMethodParams(): void $router = $this->createNewAutoRouter(); - $router->getRoute('mycontroller/somemethod/a/b', 'get'); + $router->getRoute('mycontroller/somemethod/a/b', Method::GET); } public function testAutoRouteFindsControllerWithFile(): void @@ -161,7 +164,7 @@ public function testAutoRouteFindsControllerWithFile(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller', 'get'); + = $router->getRoute('mycontroller', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -174,7 +177,7 @@ public function testAutoRouteFindsControllerWithSubfolder(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/mycontroller/somemethod', 'get'); + = $router->getRoute('subfolder/mycontroller/somemethod', Method::GET); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . Controllers\Subfolder\Mycontroller::class, $controller); @@ -192,7 +195,7 @@ public function testAutoRouteFindsControllerWithSubSubfolder() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/sub/mycontroller/somemethod', 'get'); + = $router->getRoute('subfolder/sub/mycontroller/somemethod', Method::GET); $this->assertSame('Subfolder/Sub/', $directory); $this->assertSame('\\' . Controllers\Subfolder\Sub\Mycontroller::class, $controller); @@ -205,7 +208,7 @@ public function testAutoRouteFindsDashedSubfolder(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder/mycontroller/somemethod', 'get'); + = $router->getRoute('dash-folder/mycontroller/somemethod', Method::GET); $this->assertSame('Dash_folder/', $directory); $this->assertSame( @@ -221,7 +224,7 @@ public function testAutoRouteFindsDashedController(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder/dash-controller/somemethod', 'get'); + = $router->getRoute('dash-folder/dash-controller/somemethod', Method::GET); $this->assertSame('Dash_folder/', $directory); $this->assertSame('\\' . Dash_controller::class, $controller); @@ -234,7 +237,7 @@ public function testAutoRouteFindsDashedMethod(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder/dash-controller/dash-method', 'get'); + = $router->getRoute('dash-folder/dash-controller/dash-method', Method::GET); $this->assertSame('Dash_folder/', $directory); $this->assertSame('\\' . Dash_controller::class, $controller); @@ -247,7 +250,7 @@ public function testAutoRouteFindsDefaultDashFolder(): void $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('dash-folder', 'get'); + = $router->getRoute('dash-folder', Method::GET); $this->assertSame('Dash_folder/', $directory); $this->assertSame('\\' . Home::class, $controller); @@ -260,7 +263,7 @@ public function testAutoRouteFallbackToDefaultMethod() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('index/15', 'get'); + = $router->getRoute('index/15', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Index::class, $controller); @@ -278,7 +281,7 @@ public function testAutoRouteFallbackToDefaultControllerOneParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/15', 'get'); + = $router->getRoute('subfolder/15', Method::GET); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . Controllers\Subfolder\Home::class, $controller); @@ -296,7 +299,7 @@ public function testAutoRouteFallbackToDefaultControllerTwoParams() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder/15/20', 'get'); + = $router->getRoute('subfolder/15/20', Method::GET); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . Controllers\Subfolder\Home::class, $controller); @@ -314,7 +317,7 @@ public function testAutoRouteFallbackToDefaultControllerNoParams() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('subfolder', 'get'); + = $router->getRoute('subfolder', Method::GET); $this->assertSame('Subfolder/', $directory); $this->assertSame('\\' . Controllers\Subfolder\Home::class, $controller); @@ -333,7 +336,7 @@ public function testAutoRouteRejectsSingleDot(): void $router = $this->createNewAutoRouter(); - $router->getRoute('.', 'get'); + $router->getRoute('.', Method::GET); } public function testAutoRouteRejectsDoubleDot(): void @@ -342,7 +345,7 @@ public function testAutoRouteRejectsDoubleDot(): void $router = $this->createNewAutoRouter(); - $router->getRoute('..', 'get'); + $router->getRoute('..', Method::GET); } public function testAutoRouteRejectsMidDot(): void @@ -351,7 +354,7 @@ public function testAutoRouteRejectsMidDot(): void $router = $this->createNewAutoRouter(); - $router->getRoute('foo.bar', 'get'); + $router->getRoute('foo.bar', Method::GET); } public function testRejectsDefaultControllerPath(): void @@ -360,7 +363,7 @@ public function testRejectsDefaultControllerPath(): void $router = $this->createNewAutoRouter(); - $router->getRoute('home', 'get'); + $router->getRoute('home', Method::GET); } public function testRejectsDefaultControllerAndDefaultMethodPath(): void @@ -369,7 +372,7 @@ public function testRejectsDefaultControllerAndDefaultMethodPath(): void $router = $this->createNewAutoRouter(); - $router->getRoute('home/index', 'get'); + $router->getRoute('home/index', Method::GET); } public function testRejectsDefaultMethodPath(): void @@ -378,7 +381,7 @@ public function testRejectsDefaultMethodPath(): void $router = $this->createNewAutoRouter(); - $router->getRoute('mycontroller/index', 'get'); + $router->getRoute('mycontroller/index', Method::GET); } public function testRejectsControllerWithRemapMethod(): void @@ -390,7 +393,7 @@ public function testRejectsControllerWithRemapMethod(): void $router = $this->createNewAutoRouter(); - $router->getRoute('remap/test', 'get'); + $router->getRoute('remap/test', Method::GET); } public function testRejectsURIWithUnderscoreFolder() @@ -402,7 +405,7 @@ public function testRejectsURIWithUnderscoreFolder() $router = $this->createNewAutoRouter(); - $router->getRoute('dash_folder', 'get'); + $router->getRoute('dash_folder', Method::GET); } public function testRejectsURIWithUnderscoreController() @@ -414,7 +417,7 @@ public function testRejectsURIWithUnderscoreController() $router = $this->createNewAutoRouter(); - $router->getRoute('dash-folder/dash_controller/dash-method', 'get'); + $router->getRoute('dash-folder/dash_controller/dash-method', Method::GET); } public function testRejectsURIWithUnderscoreMethod() @@ -426,7 +429,7 @@ public function testRejectsURIWithUnderscoreMethod() $router = $this->createNewAutoRouter(); - $router->getRoute('dash-folder/dash-controller/dash_method', 'get'); + $router->getRoute('dash-folder/dash-controller/dash_method', Method::GET); } public function testPermitsURIWithUnderscoreParam() @@ -434,7 +437,7 @@ public function testPermitsURIWithUnderscoreParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod/a_b', 'get'); + = $router->getRoute('mycontroller/somemethod/a_b', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); @@ -447,11 +450,134 @@ public function testDoesNotTranslateDashInParam() $router = $this->createNewAutoRouter(); [$directory, $controller, $method, $params] - = $router->getRoute('mycontroller/somemethod/a-b', 'get'); + = $router->getRoute('mycontroller/somemethod/a-b', Method::GET); $this->assertNull($directory); $this->assertSame('\\' . Mycontroller::class, $controller); $this->assertSame('getSomemethod', $method); $this->assertSame(['a-b'], $params); } + + /** + * @dataProvider provideTranslateUriToCamelCase + */ + public function testTranslateUriToCamelCase( + string $uri, + ?string $expDirectory, + string $expController, + string $expMethod, + int $controllerPos, + ?int $methodPos, + ?int $paramPos + ) { + $config = config(Routing::class); + $config->translateUriToCamelCase = true; + Factories::injectMock('config', Routing::class, $config); + + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute($uri, Method::GET); + + $this->assertSame($expDirectory, $directory); + $this->assertSame($expController, $controller); + $this->assertSame($expMethod, $method); + $this->assertSame([], $params); + $this->assertSame([ + 'controller' => $controllerPos, + 'method' => $methodPos, + 'params' => $paramPos, + ], $router->getPos()); + } + + public static function provideTranslateUriToCamelCase(): iterable + { + yield from [ + 'controller-wz-dash' => [ + 'blog-controller', + null, + '\\' . BlogController::class, + 'getIndex', + 0, + null, + null, + ], + 'controller-and-method-wz-dash' => [ + 'blog-controller/some-method', + null, + '\\' . BlogController::class, + 'getSomeMethod', + 0, + 1, + null, + ], + 'subfolder-controller-and-method-wz-dash' => [ + 'subfolder/sub/blog-controller/some-method', + 'Subfolder/Sub/', + '\\' . Controllers\Subfolder\Sub\BlogController::class, + 'getSomeMethod', + 2, + 3, + null, + ], + 'subfolder-wz-dash-controller-and-method-wz-dash' => [ + 'sub-dir/blog-controller/some-method', + 'SubDir/', + '\\' . Controllers\SubDir\BlogController::class, + 'getSomeMethod', + 1, + 2, + null, + ], + ]; + } + + /** + * @dataProvider provideRejectTranslateUriToCamelCase + */ + public function testRejectTranslateUriToCamelCase(string $uri, string $expMsg) + { + $this->expectException(PageNotFoundException::class); + $this->expectExceptionMessage( + $expMsg + ); + + $config = config(Routing::class); + $config->translateUriToCamelCase = true; + Factories::injectMock('config', Routing::class, $config); + + $router = $this->createNewAutoRouter(); + + $router->getRoute($uri, Method::GET); + } + + public static function provideRejectTranslateUriToCamelCase(): iterable + { + yield from [ + 'controller-wo-dash' => [ + 'blogcontroller', + '"\CodeIgniter\Router\Controllers\Blogcontroller" is not found.', + ], + 'controller-wz-double-dash' => [ + 'blog--controller', + 'AutoRouterImproved prohibits access to the URI containing double dash ("blog--controller") when $translateUriToCamelCase is enabled. Please use the single dash. URI:blog--controller', + ], + 'controller-wz-uppercase' => [ + 'blogController', + 'AutoRouterImproved prohibits access to the URI containing uppercase letters ("blogController") when $translateUriToCamelCase is enabled. Please use the dash. URI:blogController', + ], + 'method-wo-dash' => [ + 'blog-controller/somemethod', + '"\CodeIgniter\Router\Controllers\BlogController::getSomemethod()" is not found.', + ], + 'method-wz-uppercase' => [ + 'blog-controller/someMethod', + 'AutoRouterImproved prohibits access to the URI containing uppercase letters ("someMethod") when $translateUriToCamelCase is enabled. Please use the dash. URI:blog-controller/someMethod', + ], + 'method-wz-double-dash' => [ + 'blog-controller/some--method', + 'AutoRouterImproved prohibits access to the URI containing double dash ("some--method") when $translateUriToCamelCase is enabled. Please use the single dash. URI:blog-controller/some--method', + ], + ]; + } } diff --git a/tests/system/Router/Controllers/BlogController.php b/tests/system/Router/Controllers/BlogController.php new file mode 100644 index 000000000000..2287d8ed4dec --- /dev/null +++ b/tests/system/Router/Controllers/BlogController.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers; + +use CodeIgniter\Controller; + +class BlogController extends Controller +{ + public function getIndex(): void + { + } + + public function getSomeMethod($first = ''): void + { + } +} diff --git a/tests/system/Router/Controllers/Dash_folder/Dash_controller.php b/tests/system/Router/Controllers/Dash_folder/Dash_controller.php index 2c608a9af1e1..afe52a8df1b8 100644 --- a/tests/system/Router/Controllers/Dash_folder/Dash_controller.php +++ b/tests/system/Router/Controllers/Dash_folder/Dash_controller.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers\SubDir; + +use CodeIgniter\Controller; + +class BlogController extends Controller +{ + public function getIndex(): void + { + } + + public function getSomeMethod($first = ''): void + { + } +} diff --git a/tests/system/Router/Controllers/Subfolder/Home.php b/tests/system/Router/Controllers/Subfolder/Home.php index b249971f2516..9c28c2004a1b 100644 --- a/tests/system/Router/Controllers/Subfolder/Home.php +++ b/tests/system/Router/Controllers/Subfolder/Home.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers\Subfolder\Sub; + +use CodeIgniter\Controller; + +class BlogController extends Controller +{ + public function getIndex(): void + { + } + + public function getSomeMethod($first = ''): void + { + } +} diff --git a/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php b/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php index 7bd80203b914..6329369a60bb 100644 --- a/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php +++ b/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php @@ -1,5 +1,7 @@ enabled = false; } - return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb('get'); + return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb(Method::GET); } public function testCollect() @@ -61,25 +64,25 @@ public function testCollect() $expected = [ [ - 'method' => 'get', + 'method' => 'GET', 'route' => 'journals', 'name' => 'journals', 'handler' => '\App\Controllers\Blogs', ], [ - 'method' => 'get', + 'method' => 'GET', 'route' => 'product/([0-9]+)', 'name' => 'product/([0-9]+)', 'handler' => '\App\Controllers\Catalog::productLookupByID/$1', ], [ - 'method' => 'get', + 'method' => 'GET', 'route' => 'feed', 'name' => 'feed', 'handler' => '(Closure)', ], [ - 'method' => 'get', + 'method' => 'GET', 'route' => 'about', 'name' => 'about', 'handler' => '(View) pages/about', @@ -108,19 +111,19 @@ public function testCollectSameFromWithDifferentVerb() $expected = [ [ - 'method' => 'get', + 'method' => 'GET', 'route' => 'login', 'name' => 'loginShow', 'handler' => '\\App\\Controllers\\AuthController::showLogin', ], [ - 'method' => 'get', + 'method' => 'GET', 'route' => 'logout', 'name' => 'logout', 'handler' => '\\App\\Controllers\\AuthController::logout', ], [ - 'method' => 'post', + 'method' => 'POST', 'route' => 'login', 'name' => 'login', 'handler' => '\\App\\Controllers\\AuthController::login', diff --git a/tests/system/Router/RouteCollectionReverseRouteTest.php b/tests/system/Router/RouteCollectionReverseRouteTest.php index 02c8f4ba18f8..cbde8a16a118 100644 --- a/tests/system/Router/RouteCollectionReverseRouteTest.php +++ b/tests/system/Router/RouteCollectionReverseRouteTest.php @@ -1,5 +1,7 @@ enabled = false; } - return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb('get'); + return (new RouteCollection($loader, $moduleConfig, new Routing()))->setHTTPVerb(Method::GET); } public function testReverseRoutingEmptyString(): void diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 37844fed6cdb..86b3c8ba8d89 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1,5 +1,7 @@ defaultNamespace = '\\'; - return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb('get'); + return (new RouteCollection($loader, $moduleConfig, $routerConfig))->setHTTPVerb(Method::GET); } public function testBasicAdd(): void @@ -143,11 +147,11 @@ public function testAddIgnoresDefaultNamespaceWhenExists(): void public function testAddWorksWithCurrentHTTPMethods(): void { - Services::request()->setMethod('get'); + Services::request()->setMethod(Method::GET); $routes = $this->getCollector(); - $routes->match(['get'], 'home', 'controller'); + $routes->match(['GET'], 'home', 'controller'); $expects = [ 'home' => '\controller', @@ -175,11 +179,11 @@ public function testAddWithLeadingSlash(): void public function testMatchIgnoresInvalidHTTPMethods(): void { - Services::request()->setMethod('get'); + Services::request()->setMethod(Method::GET); $routes = $this->getCollector(); - $routes->match(['put'], 'home', 'controller'); + $routes->match(['PUT'], 'home', 'controller'); $routes = $routes->getRoutes(); @@ -188,7 +192,7 @@ public function testMatchIgnoresInvalidHTTPMethods(): void public function testAddWorksWithArrayOFHTTPMethods(): void { - Services::request()->setMethod('post'); + Services::request()->setMethod(Method::POST); $routes = $this->getCollector(); @@ -337,6 +341,104 @@ static function ($routes): void { $this->assertSame($expected, $routes->getRoutes()); } + public function testGroupNestedWithOuterOptionsWithoutInnerOptions(): void + { + $routes = $this->getCollector(); + + $routes->group( + 'admin', + ['namespace' => 'Admin', 'filter' => ['csrf']], + static function ($routes) { + $routes->get('dashboard', static function () { + }); + + $routes->group('profile', static function ($routes) { + $routes->get('/', static function () { + }); + }); + } + ); + + $expected = [ + 'admin/dashboard' => [ + 'namespace' => 'Admin', + 'filter' => ['csrf'], + ], + 'admin/profile' => [ + 'namespace' => 'Admin', + 'filter' => ['csrf'], + ], + ]; + $this->assertSame($expected, $routes->getRoutesOptions()); + } + + public function testGroupNestedWithOuterAndInnerOption(): void + { + $routes = $this->getCollector(); + + $routes->group( + 'admin', + ['filter' => ['csrf']], + static function ($routes) { + $routes->get('dashboard', static function () { + }); + + $routes->group( + 'profile', + ['filter' => ['honeypot']], + static function ($routes) { + $routes->get('/', static function () { + }); + } + ); + } + ); + + $expected = [ + 'admin/dashboard' => [ + 'filter' => ['csrf'], + ], + 'admin/profile' => [ + 'filter' => ['csrf', 'honeypot'], + ], + ]; + $this->assertSame($expected, $routes->getRoutesOptions()); + } + + public function testGroupNestedWithoutOuterOptionWithInnerOption(): void + { + $routes = $this->getCollector(); + + $routes->group( + 'admin', + ['filter' => 'csrf'], + static function ($routes) { + $routes->get('dashboard', static function () { + }); + + $routes->group( + 'profile', + ['namespace' => 'Admin'], + static function ($routes) { + $routes->get('/', static function () { + }); + } + ); + } + ); + + $expected = [ + 'admin/dashboard' => [ + 'filter' => ['csrf'], + ], + 'admin/profile' => [ + 'filter' => ['csrf'], + 'namespace' => 'Admin', + ], + ]; + $this->assertSame($expected, $routes->getRoutesOptions()); + } + public function testGroupingWorksWithEmptyStringPrefix(): void { $routes = $this->getCollector(); @@ -450,7 +552,7 @@ public function testHostnameOption(): void public function testResourceScaffoldsCorrectly(): void { $routes = $this->getCollector(); - $routes->setHTTPVerb('get'); + $routes->setHTTPVerb(Method::GET); $routes->resource('photos'); @@ -464,7 +566,7 @@ public function testResourceScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('post'); + $routes->setHTTPVerb(Method::POST); $routes->resource('photos'); $expected = [ @@ -474,7 +576,7 @@ public function testResourceScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('put'); + $routes->setHTTPVerb(Method::PUT); $routes->resource('photos'); $expected = [ @@ -484,7 +586,7 @@ public function testResourceScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('patch'); + $routes->setHTTPVerb(Method::PATCH); $routes->resource('photos'); $expected = [ @@ -494,7 +596,7 @@ public function testResourceScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('delete'); + $routes->setHTTPVerb(Method::DELETE); $routes->resource('photos'); $expected = [ @@ -509,7 +611,7 @@ public function testResourceScaffoldsCorrectly(): void public function testResourceAPIScaffoldsCorrectly(): void { $routes = $this->getCollector(); - $routes->setHTTPVerb('get'); + $routes->setHTTPVerb(Method::GET); $routes->resource('api/photos', ['controller' => 'Photos']); @@ -523,7 +625,7 @@ public function testResourceAPIScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('post'); + $routes->setHTTPVerb(Method::POST); $routes->resource('api/photos', ['controller' => 'Photos']); $expected = [ @@ -533,7 +635,7 @@ public function testResourceAPIScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('put'); + $routes->setHTTPVerb(Method::PUT); $routes->resource('api/photos', ['controller' => 'Photos']); $expected = [ @@ -543,7 +645,7 @@ public function testResourceAPIScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('patch'); + $routes->setHTTPVerb(Method::PATCH); $routes->resource('api/photos', ['controller' => 'Photos']); $expected = [ @@ -553,7 +655,7 @@ public function testResourceAPIScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('delete'); + $routes->setHTTPVerb(Method::DELETE); $routes->resource('api/photos', ['controller' => 'Photos']); $expected = [ @@ -566,7 +668,7 @@ public function testResourceAPIScaffoldsCorrectly(): void public function testPresenterScaffoldsCorrectly(): void { $routes = $this->getCollector(); - $routes->setHTTPVerb('get'); + $routes->setHTTPVerb(Method::GET); $routes->presenter('photos'); @@ -582,7 +684,7 @@ public function testPresenterScaffoldsCorrectly(): void $this->assertSame($expected, $routes->getRoutes()); $routes = $this->getCollector(); - $routes->setHTTPVerb('post'); + $routes->setHTTPVerb(Method::POST); $routes->presenter('photos'); $expected = [ @@ -597,7 +699,7 @@ public function testPresenterScaffoldsCorrectly(): void public function testResourcesWithCustomController(): void { - Services::request()->setMethod('get'); + Services::request()->setMethod(Method::GET); $routes = $this->getCollector(); $routes->resource('photos', ['controller' => '