diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 17e6b415ba0d..cf018d3c39d8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -29,6 +29,7 @@ body: - '8.0' - '8.1' - '8.2' + - '8.3' validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 98aa948b795e..f16a793ece5b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,10 +2,11 @@ Each pull request should address a single issue and have a meaningful title. +- PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion. - Pull requests must be in English. - If a pull request fixes an issue, reference the issue with a suitable keyword (e.g., Fixes ). - All bug fixes should be sent to the __"develop"__ branch, this is where the next bug fix version will be developed. -- PRs with any enhancement should be sent to the next minor version branch, e.g. __"4.3"__ +- PRs with any enhancement should be sent to the next minor version branch, e.g. __"4.5"__ --> **Description** diff --git a/.github/prlint.json b/.github/prlint.json new file mode 100644 index 000000000000..b8dec09f108d --- /dev/null +++ b/.github/prlint.json @@ -0,0 +1,8 @@ +{ + "title": [ + { + "pattern": "^(\\[\\d+\\.\\d+\\]\\s{1})?(feat|fix|chore|docs|perf|refactor|style|test)(\\([\\-.@:`a-zA-Z0-9]+\\))?!?:\\s{1}\\S.+\\S|Prep for \\d\\.\\d\\.\\d release|\\d\\.\\d\\.\\d Ready code$", + "message": "PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion." + } + ] +} diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index ab366268e5ee..8c1545286fe9 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -48,7 +48,7 @@ jobs: # Create an artifact of the html output - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: HTML Documentation path: user_guide_src/build/html/ diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index a5dfc83996cb..0e1df16d4ef5 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -24,7 +24,7 @@ jobs: coverage: xdebug - name: Download coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: build/cov @@ -37,7 +37,7 @@ jobs: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -46,7 +46,7 @@ jobs: ${{ github.job }}- - name: Cache PHPUnit's static analysis cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 0963f5865242..4943d3f3ac2c 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -167,7 +167,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}-${{ hashFiles('**/composer.*') }} @@ -178,7 +178,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} @@ -206,7 +206,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.ARTIFACT_NAME }} path: build/cov/coverage-${{ env.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 6371847c77bd..8a9f00c5e2e6 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -79,7 +79,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}" >> $GITHUB_ENV - name: Cache Composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -89,7 +89,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} @@ -116,7 +116,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.ARTIFACT_NAME }} path: build/cov/coverage-${{ env.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 9217f48bb127..5461dda461bd 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -46,7 +46,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/test-deptrac.yml b/.github/workflows/test-deptrac.yml index b92f861c790c..67c9843f334e 100644 --- a/.github/workflows/test-deptrac.yml +++ b/.github/workflows/test-deptrac.yml @@ -53,7 +53,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -63,7 +63,7 @@ jobs: run: mkdir -p build/ - name: Cache Deptrac results - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build key: ${{ runner.os }}-deptrac-${{ github.sha }} diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 3ab82f25c762..a8e6eaefcb49 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -63,7 +63,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -73,7 +73,7 @@ jobs: run: mkdir -p build/phpstan - name: Cache PHPStan result cache directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build/phpstan key: ${{ runner.os }}-phpstan-${{ github.sha }} diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 62e65bf7a866..98ca5c44c5b3 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -44,7 +44,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} @@ -54,7 +54,7 @@ jobs: run: mkdir -p build/psalm - name: Cache Psalm results - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: build/psalm key: ${{ runner.os }}-psalm-${{ github.sha }} diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 7912a59f5d3f..9d9e402ff035 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -71,7 +71,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 8c2a6b8b7a52..3653cf1349c2 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -43,31 +43,7 @@ __DIR__ . '/spark', ]); -$overrides = [ - 'php_unit_data_provider_name' => [ - 'prefix' => 'provide', - 'suffix' => '', - ], - 'php_unit_data_provider_static' => true, - 'php_unit_data_provider_return_type' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - 'attribute', - 'break', - 'case', - 'continue', - 'curly_brace_block', - 'default', - 'extra', - 'parenthesis_brace_block', - 'return', - 'square_brace_block', - 'switch', - 'throw', - 'use', - ], - ], -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.cache', diff --git a/.php-cs-fixer.no-header.php b/.php-cs-fixer.no-header.php index 5fb4ce95bfbe..7c9ae1e80737 100644 --- a/.php-cs-fixer.no-header.php +++ b/.php-cs-fixer.no-header.php @@ -29,31 +29,7 @@ __DIR__ . '/admin/starter/builds', ]); -$overrides = [ - 'php_unit_data_provider_name' => [ - 'prefix' => 'provide', - 'suffix' => '', - ], - 'php_unit_data_provider_static' => true, - 'php_unit_data_provider_return_type' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - 'attribute', - 'break', - 'case', - 'continue', - 'curly_brace_block', - 'default', - 'extra', - 'parenthesis_brace_block', - 'return', - 'square_brace_block', - 'switch', - 'throw', - 'use', - ], - ], -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.no-header.cache', diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php index cf344d903ad5..fe634f0a6f21 100644 --- a/.php-cs-fixer.user-guide.php +++ b/.php-cs-fixer.user-guide.php @@ -31,32 +31,13 @@ ]); $overrides = [ - 'echo_tag_syntax' => false, - 'php_unit_internal_class' => false, - 'no_unused_imports' => false, - 'class_attributes_separation' => false, - 'php_unit_data_provider_return_type' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - 'attribute', - 'break', - 'case', - 'continue', - 'curly_brace_block', - 'default', - 'extra', - 'parenthesis_brace_block', - 'return', - 'square_brace_block', - 'switch', - 'throw', - 'use', - ], - ], - 'php_unit_data_provider_static' => true, - 'php_unit_data_provider_name' => [ - 'prefix' => 'provide', - 'suffix' => '', + 'echo_tag_syntax' => false, + 'php_unit_internal_class' => false, + 'no_unused_imports' => false, + 'class_attributes_separation' => false, + 'fully_qualified_strict_types' => [ + 'import_symbols' => false, + 'leading_backslash_in_global_namespace' => true, ], ]; diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b773f58ac4..c7c4e4dd3254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [v4.4.5](https://github.com/codeigniter4/CodeIgniter4/tree/v4.4.5) (2024-01-27) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.4.4...v4.4.5) + +### Fixed Bugs + +* fix: bug 4.4.4 `spark serve` not working when using Session in Routes.php by @ALTITUDE-DEV-FR in https://github.com/codeigniter4/CodeIgniter4/pull/8389 +* fix: `highlightFile()` in `BaseExceptionHandler` for PHP 8.3 by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/8401 +* fix: [Validation] DotArrayFilter returns incorrect array when numeric index array is passed by @grimpirate in https://github.com/codeigniter4/CodeIgniter4/pull/8425 +* fix: OCI8 Forge always sets NOT NULL when BOOLEAN is specified by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8440 +* fix: DB Seeder may use wrong DB connection during testing by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8447 +* fix: [Postgre] QueryBuilder::updateBatch() does not work (No API change) by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8439 +* fix: [Postgre] QueryBuilder::deleteBatch() does not work by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8451 +* fix: [Email] setAttachmentCID() does not work with buffer string by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8446 +* fix: add undocumented Model $allowEmptyInserts by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8456 + +### Refactoring + +* refactor: remove overrides for coding-standard v1.7.12 by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/8386 +* refactor: Table class to fix phpstan errors by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8402 +* fix: typo in pager default_simple by @jasonliang-dev in https://github.com/codeigniter4/CodeIgniter4/pull/8407 +* refactor: improve Forge variable names by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8434 + ## [v4.4.4](https://github.com/codeigniter4/CodeIgniter4/tree/v4.4.4) (2023-12-28) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.4.3...v4.4.4) diff --git a/README.md b/README.md index 1a47301b82cc..e8a48b0e0205 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CodeIgniter 4 Development -[![PHPUnit](https://github.com/codeigniter4/CodeIgniter4/workflows/PHPUnit/badge.svg)](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-phpunit.yml) +[![PHPUnit](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-phpunit.yml/badge.svg)](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-phpunit.yml) [![PHPStan](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-phpstan.yml/badge.svg)](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-phpstan.yml) [![Psalm](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-psalm.yml/badge.svg)](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/test-psalm.yml) [![Coverage Status](https://coveralls.io/repos/github/codeigniter4/CodeIgniter4/badge.svg?branch=develop)](https://coveralls.io/github/codeigniter4/CodeIgniter4?branch=develop) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 8d68ccd9923a..b168f7856354 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -6,7 +6,21 @@ > > -MGatner, kenjis -## [Minor version only] Merge minor version branch into develop +## Merge `develop` branch into next minor version branch `4.x` + +Before starting release process, if there are commits in `develop` branch that +are not merged into `4.x` branch, merge them. This is because if conflicts occur, +merging will take time. + +```console +git fetch upstream +git switch 4.x +git merge upstream/4.x +git merge upstream/develop +git push upstream HEAD +``` + +## [Minor version only] Merge minor version branch into `develop` If you release a new minor version. @@ -149,7 +163,7 @@ Work off direct clones of the repos so the release branches persist for a time. git merge origin/master git push origin HEAD ``` -* [ ] Update the next minor upgrade branch `4.x`: +* [ ] Update the next minor version branch `4.x`: ```console git fetch origin git checkout 4.x @@ -157,7 +171,7 @@ Work off direct clones of the repos so the release branches persist for a time. git merge origin/develop git push origin HEAD ``` -* [ ] [Minor version only] Create the next minor upgrade branch `4.x`: +* [ ] [Minor version only] Create the new next minor version branch `4.x`: ```console git fetch origin git switch develop diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 9d7cba44ddbb..891bff556031 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -18,9 +18,9 @@ "psr/log": "^1.1" }, "require-dev": { - "codeigniter/coding-standard": "^1.5", + "codeigniter/coding-standard": "^1.7", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "~3.41.0", + "friendsofphp/php-cs-fixer": "^3.47.1", "kint-php/kint": "^5.0.4", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", diff --git a/composer.json b/composer.json index f7f98d862c11..82c97606b4c8 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,11 @@ "psr/log": "^1.1" }, "require-dev": { - "codeigniter/coding-standard": "^1.5", + "codeigniter/coding-standard": "^1.7", "codeigniter/phpstan-codeigniter": "^1.4", "ergebnis/composer-normalize": "^2.28", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "~3.41.0", + "friendsofphp/php-cs-fixer": "^3.47.1", "kint-php/kint": "^5.0.4", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", @@ -34,7 +34,7 @@ "phpunit/phpcov": "^8.2", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0", - "rector/rector": "0.18.13", + "rector/rector": "0.19.2", "vimeo/psalm": "^5.0" }, "replace": { @@ -91,8 +91,7 @@ }, "scripts": { "post-update-cmd": [ - "CodeIgniter\\ComposerScripts::postUpdate", - "bash -c \"if [ -f admin/setup.sh ]; then bash admin/setup.sh; fi\"" + "CodeIgniter\\ComposerScripts::postUpdate" ], "analyze": [ "Composer\\Config::disableProcessTimeout", diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index 4fb8fd62693f..f0b3a7408da4 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -10,7 +10,7 @@ api/build/ api/cache/ - + system diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 1d32322ab5d4..22aae16b046e 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -1341,11 +1341,6 @@ 'count' => 7, 'path' => __DIR__ . '/system/Database/Postgre/Builder.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Only booleans are allowed in a negated boolean, array\\\\|string\\> given\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/Postgre/Builder.php', -]; $ignoreErrors[] = [ 'message' => '#^Return type \\(CodeIgniter\\\\Database\\\\BaseBuilder\\) of method CodeIgniter\\\\Database\\\\Postgre\\\\Builder\\:\\:join\\(\\) should be covariant with return type \\(\\$this\\(CodeIgniter\\\\Database\\\\BaseBuilder\\)\\) of method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:join\\(\\)$#', 'count' => 1, @@ -1451,11 +1446,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Database/Postgre/Forge.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Return type \\(array\\|bool\\|string\\) of method CodeIgniter\\\\Database\\\\Postgre\\\\Forge\\:\\:_alterTable\\(\\) should be covariant with return type \\(array\\\\|string\\|false\\) of method CodeIgniter\\\\Database\\\\Forge\\:\\:_alterTable\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/Postgre/Forge.php', -]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 1, @@ -1656,21 +1646,11 @@ 'count' => 1, 'path' => __DIR__ . '/system/Database/SQLite3/Forge.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Return type \\(array\\|string\\|null\\) of method CodeIgniter\\\\Database\\\\SQLite3\\\\Forge\\:\\:_alterTable\\(\\) should be covariant with return type \\(array\\\\|string\\|false\\) of method CodeIgniter\\\\Database\\\\Forge\\:\\:_alterTable\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Database/SQLite3/Forge.php', -]; $ignoreErrors[] = [ 'message' => '#^Return type \\(SQLite3Result\\|false\\) of method CodeIgniter\\\\Database\\\\SQLite3\\\\PreparedQuery\\:\\:_getResult\\(\\) should be covariant with return type \\(object\\|resource\\|null\\) of method CodeIgniter\\\\Database\\\\BasePreparedQuery\\\\:\\:_getResult\\(\\)$#', 'count' => 1, 'path' => __DIR__ . '/system/Database/SQLite3/PreparedQuery.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Class stdClass referenced with incorrect case\\: stdclass\\.$#', - 'count' => 2, - 'path' => __DIR__ . '/system/Database/SQLite3/Table.php', -]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 2, @@ -1846,11 +1826,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Exceptions/PageNotFoundException.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to method SplFileInfo\\:\\:getBasename\\(\\) with incorrect case\\: getBaseName$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Files/File.php', -]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 1, @@ -2491,11 +2466,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/Pager/Pager.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Call to method CodeIgniter\\\\Pager\\\\PagerRenderer\\:\\:getNext\\(\\) with incorrect case\\: getnext$#', - 'count' => 1, - 'path' => __DIR__ . '/system/Pager/Views/default_simple.php', -]; $ignoreErrors[] = [ 'message' => '#^Argument \\#1 \\$name \\(class\\-string\\) passed to function model does not extend CodeIgniter\\\\\\\\Model\\.$#', 'count' => 1, diff --git a/rector.php b/rector.php index 5be63b31e59d..b1520c38e10a 100644 --- a/rector.php +++ b/rector.php @@ -59,7 +59,7 @@ PHPUnitSetList::PHPUNIT_100, ]); - $rectorConfig->parallel(); + $rectorConfig->parallel(120, 8, 15); // paths to refactor; solid alternative to CLI arguments $rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils']); diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index bc23a6e51a7c..46eb419f7717 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -54,7 +54,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.4.4'; + public const CI_VERSION = '4.4.5'; /** * App startup time. diff --git a/system/Commands/Generators/Views/model.tpl.php b/system/Commands/Generators/Views/model.tpl.php index b9b9c99eb560..72509cdbd9d4 100644 --- a/system/Commands/Generators/Views/model.tpl.php +++ b/system/Commands/Generators/Views/model.tpl.php @@ -17,6 +17,8 @@ class {class} extends Model protected $protectFields = true; protected $allowedFields = []; + protected bool $allowEmptyInserts = false; + // Dates protected $useTimestamps = false; protected $dateFormat = 'datetime'; diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 9d43e5577943..48302ab26fc7 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -169,8 +169,11 @@ class BaseBuilder * constraints?: array, * setQueryAsData?: string, * sql?: string, - * alias?: string + * alias?: string, + * fieldTypes?: array> * } + * + * fieldTypes: [ProtectedTableName => [FieldName => Type]] */ protected $QBOptions; @@ -1758,6 +1761,8 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo /** * Compiles batch insert/update/upsert strings and runs the queries * + * @param '_deleteBatch'|'_insertBatch'|'_updateBatch'|'_upsertBatch' $renderMethod + * * @return false|int|string[] Number of rows inserted or FALSE on failure, SQL array when testMode * * @throws DatabaseException diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 57e43bf199fe..adbdc208fc47 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -32,7 +32,7 @@ class Forge /** * List of fields. * - * @var array + * @var array [name => attributes] */ protected $fields = []; @@ -351,14 +351,14 @@ public function addUniqueKey($key, string $keyName = '') /** * Add Field * - * @param array|string $field + * @param array|string $fields Field array or Field string * * @return Forge */ - public function addField($field) + public function addField($fields) { - if (is_string($field)) { - if ($field === 'id') { + if (is_string($fields)) { + if ($fields === 'id') { $this->addField([ 'id' => [ 'type' => 'INT', @@ -368,27 +368,27 @@ public function addField($field) ]); $this->addKey('id', true); } else { - if (strpos($field, ' ') === false) { + if (strpos($fields, ' ') === false) { throw new InvalidArgumentException('Field information is required for that operation.'); } - $fieldName = explode(' ', $field, 2)[0]; + $fieldName = explode(' ', $fields, 2)[0]; $fieldName = trim($fieldName, '`\'"'); - $this->fields[$fieldName] = $field; + $this->fields[$fieldName] = $fields; } } - if (is_array($field)) { - foreach ($field as $idx => $f) { - if (is_string($f)) { - $this->addField($f); + if (is_array($fields)) { + foreach ($fields as $name => $attributes) { + if (is_string($attributes)) { + $this->addField($attributes); continue; } - if (is_array($f)) { - $this->fields = array_merge($this->fields, [$idx => $f]); + if (is_array($attributes)) { + $this->fields = array_merge($this->fields, [$name => $attributes]); } } } @@ -404,8 +404,14 @@ public function addField($field) * * @throws DatabaseException */ - public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = ''): Forge - { + public function addForeignKey( + $fieldName = '', + string $tableName = '', + $tableField = '', + string $onUpdate = '', + string $onDelete = '', + string $fkName = '' + ): Forge { $fieldName = (array) $fieldName; $tableField = (array) $tableField; @@ -428,8 +434,9 @@ public function addForeignKey($fieldName = '', string $tableName = '', $tableFie */ public function dropKey(string $table, string $keyName, bool $prefixKeyName = true): bool { - $keyName = $this->db->escapeIdentifiers(($prefixKeyName === true ? $this->db->DBPrefix : '') . $keyName); - $table = $this->db->escapeIdentifiers($this->db->DBPrefix . $table); + $keyName = $this->db->escapeIdentifiers(($prefixKeyName === true ? $this->db->DBPrefix : '') . $keyName); + $table = $this->db->escapeIdentifiers($this->db->DBPrefix . $table); + $dropKeyAsConstraint = $this->dropKeyAsConstraint($table, $keyName); if ($dropKeyAsConstraint === true) { @@ -458,7 +465,7 @@ public function dropKey(string $table, string $keyName, bool $prefixKeyName = tr } /** - * Checks if if key needs to be dropped as a constraint. + * Checks if key needs to be dropped as a constraint. */ protected function dropKeyAsConstraint(string $table, string $constraintName): bool { @@ -494,7 +501,7 @@ public function dropPrimaryKey(string $table, string $keyName = ''): bool } /** - * @return BaseResult|bool|false|mixed|Query + * @return bool * * @throws DatabaseException */ @@ -518,7 +525,9 @@ public function dropForeignKey(string $table, string $foreignName) } /** - * @return mixed + * @param array $attributes Table attributes + * + * @return bool * * @throws DatabaseException */ @@ -562,28 +571,30 @@ public function createTable(string $table, bool $ifNotExists = false, array $att } /** + * @param array $attributes Table attributes + * * @return string SQL string * * @deprecated $ifNotExists is no longer used, and will be removed. */ protected function _createTable(string $table, bool $ifNotExists, array $attributes) { - $columns = $this->_processFields(true); + $processedFields = $this->_processFields(true); - for ($i = 0, $c = count($columns); $i < $c; $i++) { - $columns[$i] = ($columns[$i]['_literal'] !== false) ? "\n\t" . $columns[$i]['_literal'] - : "\n\t" . $this->_processColumn($columns[$i]); + for ($i = 0, $c = count($processedFields); $i < $c; $i++) { + $processedFields[$i] = ($processedFields[$i]['_literal'] !== false) ? "\n\t" . $processedFields[$i]['_literal'] + : "\n\t" . $this->_processColumn($processedFields[$i]); } - $columns = implode(',', $columns); + $processedFields = implode(',', $processedFields); - $columns .= $this->_processPrimaryKeys($table); - $columns .= current($this->_processForeignKeys($table)); + $processedFields .= $this->_processPrimaryKeys($table); + $processedFields .= current($this->_processForeignKeys($table)); if ($this->createTableKeys === true) { $indexes = current($this->_processIndexes($table)); if (is_string($indexes)) { - $columns .= $indexes; + $processedFields .= $indexes; } } @@ -591,7 +602,7 @@ protected function _createTable(string $table, bool $ifNotExists, array $attribu $this->createTableStr . '%s', 'CREATE TABLE', $this->db->escapeIdentifiers($table), - $columns, + $processedFields, $this->_createTableAttributes($attributes) ); } @@ -610,7 +621,7 @@ protected function _createTableAttributes(array $attributes): string } /** - * @return mixed + * @return bool * * @throws DatabaseException */ @@ -676,7 +687,7 @@ protected function _dropTable(string $table, bool $ifExists, bool $cascade) } /** - * @return mixed + * @return bool * * @throws DatabaseException */ @@ -716,23 +727,24 @@ public function renameTable(string $tableName, string $newTableName) } /** - * @param array|string $field + * @param array|string $fields Field array or Field string * * @throws DatabaseException */ - public function addColumn(string $table, $field): bool + public function addColumn(string $table, $fields): bool { // Work-around for literal column definitions - if (! is_array($field)) { - $field = [$field]; + if (is_string($fields)) { + $fields = [$fields]; } - foreach (array_keys($field) as $k) { - $this->addField([$k => $field[$k]]); + foreach (array_keys($fields) as $name) { + $this->addField([$name => $fields[$name]]); } $sqls = $this->_alterTable('ADD', $this->db->DBPrefix . $table, $this->_processFields()); $this->reset(); + if ($sqls === false) { if ($this->db->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); @@ -751,15 +763,16 @@ public function addColumn(string $table, $field): bool } /** - * @param array|string $columnName + * @param array|string $columnNames column names to DROP * - * @return mixed + * @return bool * * @throws DatabaseException */ - public function dropColumn(string $table, $columnName) + public function dropColumn(string $table, $columnNames) { - $sql = $this->_alterTable('DROP', $this->db->DBPrefix . $table, $columnName); + $sql = $this->_alterTable('DROP', $this->db->DBPrefix . $table, $columnNames); + if ($sql === false) { if ($this->db->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); @@ -772,19 +785,19 @@ public function dropColumn(string $table, $columnName) } /** - * @param array|string $field + * @param array|string $fields Field array or Field string * * @throws DatabaseException */ - public function modifyColumn(string $table, $field): bool + public function modifyColumn(string $table, $fields): bool { // Work-around for literal column definitions - if (! is_array($field)) { - $field = [$field]; + if (is_string($fields)) { + $fields = [$fields]; } - foreach (array_keys($field) as $k) { - $this->addField([$k => $field[$k]]); + foreach (array_keys($fields) as $name) { + $this->addField([$name => $fields[$name]]); } if ($this->fields === []) { @@ -793,6 +806,7 @@ public function modifyColumn(string $table, $field): bool $sqls = $this->_alterTable('CHANGE', $this->db->DBPrefix . $table, $this->_processFields()); $this->reset(); + if ($sqls === false) { if ($this->db->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); @@ -813,48 +827,53 @@ public function modifyColumn(string $table, $field): bool } /** - * @param array|string $fields + * @param 'ADD'|'CHANGE'|'DROP' $alterType + * @param array|string $processedFields Processed column definitions + * or column names to DROP * - * @return false|string|string[] + * @return false|list|string|null SQL string + * @phpstan-return ($alterType is 'DROP' ? string : list|false|null) */ - protected function _alterTable(string $alterType, string $table, $fields) + protected function _alterTable(string $alterType, string $table, $processedFields) { $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) . ' '; // DROP has everything it needs now. if ($alterType === 'DROP') { - if (is_string($fields)) { - $fields = explode(',', $fields); + $columnNamesToDrop = $processedFields; + + if (is_string($columnNamesToDrop)) { + $columnNamesToDrop = explode(',', $columnNamesToDrop); } - $fields = array_map(fn ($field) => 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field)), $fields); + $columnNamesToDrop = array_map(fn ($field) => 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field)), $columnNamesToDrop); - return $sql . implode(', ', $fields); + return $sql . implode(', ', $columnNamesToDrop); } $sql .= ($alterType === 'ADD') ? 'ADD ' : $alterType . ' COLUMN '; $sqls = []; - foreach ($fields as $data) { - $sqls[] = $sql . ($data['_literal'] !== false - ? $data['_literal'] - : $this->_processColumn($data)); + foreach ($processedFields as $field) { + $sqls[] = $sql . ($field['_literal'] !== false + ? $field['_literal'] + : $this->_processColumn($field)); } return $sqls; } /** - * Process fields + * Returns $processedFields array from $this->fields data. */ protected function _processFields(bool $createTable = false): array { - $fields = []; + $processedFields = []; - foreach ($this->fields as $key => $attributes) { + foreach ($this->fields as $name => $attributes) { if (! is_array($attributes)) { - $fields[] = ['_literal' => $attributes]; + $processedFields[] = ['_literal' => $attributes]; continue; } @@ -870,7 +889,7 @@ protected function _processFields(bool $createTable = false): array } $field = [ - 'name' => $key, + 'name' => $name, 'new_name' => $attributes['NAME'] ?? null, 'type' => $attributes['TYPE'] ?? null, 'length' => '', @@ -928,24 +947,24 @@ protected function _processFields(bool $createTable = false): array $field['length'] = '(' . $attributes['CONSTRAINT'] . ')'; } - $fields[] = $field; + $processedFields[] = $field; } - return $fields; + return $processedFields; } /** - * Process column + * Converts $processedField array to field definition string. */ - protected function _processColumn(array $field): string + protected function _processColumn(array $processedField): string { - return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] . $field['length'] - . $field['unsigned'] - . $field['default'] - . $field['null'] - . $field['auto_increment'] - . $field['unique']; + return $this->db->escapeIdentifiers($processedField['name']) + . ' ' . $processedField['type'] . $processedField['length'] + . $processedField['unsigned'] + . $processedField['default'] + . $processedField['null'] + . $processedField['auto_increment'] + . $processedField['unique']; } /** @@ -1163,10 +1182,10 @@ protected function _processForeignKeys(string $table, bool $asQuery = false): ar { $errorNames = []; - foreach ($this->foreignKeys as $name) { - foreach ($name['field'] as $f) { - if (! isset($this->fields[$f])) { - $errorNames[] = $f; + foreach ($this->foreignKeys as $fkeyInfo) { + foreach ($fkeyInfo['field'] as $fieldName) { + if (! isset($this->fields[$fieldName])) { + $errorNames[] = $fieldName; } } } diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index 0d294c2d09a2..b1beba2a9dd2 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -128,57 +128,59 @@ protected function _createTableAttributes(array $attributes): string /** * ALTER TABLE * - * @param string $alterType ALTER type - * @param string $table Table name - * @param array|string $field Column definition + * @param string $alterType ALTER type + * @param string $table Table name + * @param array|string $processedFields Processed column definitions + * or column names to DROP * - * @return string|string[] + * @return list|string SQL string + * @phpstan-return ($alterType is 'DROP' ? string : list) */ - protected function _alterTable(string $alterType, string $table, $field) + protected function _alterTable(string $alterType, string $table, $processedFields) { if ($alterType === 'DROP') { - return parent::_alterTable($alterType, $table, $field); + return parent::_alterTable($alterType, $table, $processedFields); } $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); - foreach ($field as $i => $data) { - if ($data['_literal'] !== false) { - $field[$i] = ($alterType === 'ADD') ? "\n\tADD " . $data['_literal'] : "\n\tMODIFY " . $data['_literal']; + foreach ($processedFields as $i => $field) { + if ($field['_literal'] !== false) { + $processedFields[$i] = ($alterType === 'ADD') ? "\n\tADD " . $field['_literal'] : "\n\tMODIFY " . $field['_literal']; } else { if ($alterType === 'ADD') { - $field[$i]['_literal'] = "\n\tADD "; + $processedFields[$i]['_literal'] = "\n\tADD "; } else { - $field[$i]['_literal'] = empty($data['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + $processedFields[$i]['_literal'] = empty($field['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; } - $field[$i] = $field[$i]['_literal'] . $this->_processColumn($field[$i]); + $processedFields[$i] = $processedFields[$i]['_literal'] . $this->_processColumn($processedFields[$i]); } } - return [$sql . implode(',', $field)]; + return [$sql . implode(',', $processedFields)]; } /** * Process column */ - protected function _processColumn(array $field): string + protected function _processColumn(array $processedField): string { - $extraClause = isset($field['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($field['after']) : ''; + $extraClause = isset($processedField['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($processedField['after']) : ''; - if (empty($extraClause) && isset($field['first']) && $field['first'] === true) { + if (empty($extraClause) && isset($processedField['first']) && $processedField['first'] === true) { $extraClause = ' FIRST'; } - return $this->db->escapeIdentifiers($field['name']) - . (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name'])) - . ' ' . $field['type'] . $field['length'] - . $field['unsigned'] - . $field['null'] - . $field['default'] - . $field['auto_increment'] - . $field['unique'] - . (empty($field['comment']) ? '' : ' COMMENT ' . $field['comment']) + return $this->db->escapeIdentifiers($processedField['name']) + . (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name'])) + . ' ' . $processedField['type'] . $processedField['length'] + . $processedField['unsigned'] + . $processedField['null'] + . $processedField['default'] + . $processedField['auto_increment'] + . $processedField['unique'] + . (empty($processedField['comment']) ? '' : ' COMMENT ' . $processedField['comment']) . $extraClause; } diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 6e1e85f666a1..2735b886cb86 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -93,21 +93,29 @@ class Forge extends BaseForge /** * ALTER TABLE * - * @param string $alterType ALTER type - * @param string $table Table name - * @param array|string $field Column definition + * @param string $alterType ALTER type + * @param string $table Table name + * @param array|string $processedFields Processed column definitions + * or column names to DROP * - * @return string|string[] + * @return list|string SQL string + * @phpstan-return ($alterType is 'DROP' ? string : list) */ - protected function _alterTable(string $alterType, string $table, $field) + protected function _alterTable(string $alterType, string $table, $processedFields) { $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); if ($alterType === 'DROP') { - $fields = array_map(fn ($field) => $this->db->escapeIdentifiers(trim($field)), is_string($field) ? explode(',', $field) : $field); + $columnNamesToDrop = $processedFields; + + $fields = array_map( + fn ($field) => $this->db->escapeIdentifiers(trim($field)), + is_string($columnNamesToDrop) ? explode(',', $columnNamesToDrop) : $columnNamesToDrop + ); return $sql . ' DROP (' . implode(',', $fields) . ') CASCADE CONSTRAINT INVALIDATE'; } + if ($alterType === 'CHANGE') { $alterType = 'MODIFY'; } @@ -115,50 +123,50 @@ protected function _alterTable(string $alterType, string $table, $field) $nullableMap = array_column($this->db->getFieldData($table), 'nullable', 'name'); $sqls = []; - for ($i = 0, $c = count($field); $i < $c; $i++) { + for ($i = 0, $c = count($processedFields); $i < $c; $i++) { if ($alterType === 'MODIFY') { // If a null constraint is added to a column with a null constraint, // ORA-01451 will occur, // so add null constraint is used only when it is different from the current null constraint. // If a not null constraint is added to a column with a not null constraint, // ORA-01442 will occur. - $wantToAddNull = strpos($field[$i]['null'], ' NOT') === false; - $currentNullable = $nullableMap[$field[$i]['name']]; + $wantToAddNull = strpos($processedFields[$i]['null'], ' NOT') === false; + $currentNullable = $nullableMap[$processedFields[$i]['name']]; if ($wantToAddNull === true && $currentNullable === true) { - $field[$i]['null'] = ''; - } elseif ($field[$i]['null'] === '' && $currentNullable === false) { + $processedFields[$i]['null'] = ''; + } elseif ($processedFields[$i]['null'] === '' && $currentNullable === false) { // Nullable by default - $field[$i]['null'] = ' NULL'; + $processedFields[$i]['null'] = ' NULL'; } elseif ($wantToAddNull === false && $currentNullable === false) { - $field[$i]['null'] = ''; + $processedFields[$i]['null'] = ''; } } - if ($field[$i]['_literal'] !== false) { - $field[$i] = "\n\t" . $field[$i]['_literal']; + if ($processedFields[$i]['_literal'] !== false) { + $processedFields[$i] = "\n\t" . $processedFields[$i]['_literal']; } else { - $field[$i]['_literal'] = "\n\t" . $this->_processColumn($field[$i]); + $processedFields[$i]['_literal'] = "\n\t" . $this->_processColumn($processedFields[$i]); - if (! empty($field[$i]['comment'])) { + if (! empty($processedFields[$i]['comment'])) { $sqls[] = 'COMMENT ON COLUMN ' - . $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($field[$i]['name']) - . ' IS ' . $field[$i]['comment']; + . $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($processedFields[$i]['name']) + . ' IS ' . $processedFields[$i]['comment']; } - if ($alterType === 'MODIFY' && ! empty($field[$i]['new_name'])) { - $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name']) - . ' TO ' . $this->db->escapeIdentifiers($field[$i]['new_name']); + if ($alterType === 'MODIFY' && ! empty($processedFields[$i]['new_name'])) { + $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($processedFields[$i]['name']) + . ' TO ' . $this->db->escapeIdentifiers($processedFields[$i]['new_name']); } - $field[$i] = "\n\t" . $field[$i]['_literal']; + $processedFields[$i] = "\n\t" . $processedFields[$i]['_literal']; } } $sql .= ' ' . $alterType . ' '; - $sql .= count($field) === 1 - ? $field[0] - : '(' . implode(',', $field) . ')'; + $sql .= count($processedFields) === 1 + ? $processedFields[0] + : '(' . implode(',', $processedFields) . ')'; // RENAME COLUMN must be executed after MODIFY array_unshift($sqls, $sql); @@ -184,26 +192,26 @@ protected function _attributeAutoIncrement(array &$attributes, array &$field) /** * Process column */ - protected function _processColumn(array $field): string + protected function _processColumn(array $processedField): string { $constraint = ''; // @todo: can't cover multi pattern when set type. - if ($field['type'] === 'VARCHAR2' && strpos($field['length'], "('") === 0) { - $constraint = ' CHECK(' . $this->db->escapeIdentifiers($field['name']) - . ' IN ' . $field['length'] . ')'; + if ($processedField['type'] === 'VARCHAR2' && strpos($processedField['length'], "('") === 0) { + $constraint = ' CHECK(' . $this->db->escapeIdentifiers($processedField['name']) + . ' IN ' . $processedField['length'] . ')'; - $field['length'] = '(' . max(array_map('mb_strlen', explode("','", mb_substr($field['length'], 2, -2)))) . ')' . $constraint; - } elseif (isset($this->primaryKeys['fields']) && count($this->primaryKeys['fields']) === 1 && $field['name'] === $this->primaryKeys['fields'][0]) { - $field['unique'] = ''; + $processedField['length'] = '(' . max(array_map('mb_strlen', explode("','", mb_substr($processedField['length'], 2, -2)))) . ')' . $constraint; + } elseif (isset($this->primaryKeys['fields']) && count($this->primaryKeys['fields']) === 1 && $processedField['name'] === $this->primaryKeys['fields'][0]) { + $processedField['unique'] = ''; } - return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] . $field['length'] - . $field['unsigned'] - . $field['default'] - . $field['auto_increment'] - . $field['null'] - . $field['unique']; + return $this->db->escapeIdentifiers($processedField['name']) + . ' ' . $processedField['type'] . $processedField['length'] + . $processedField['unsigned'] + . $processedField['default'] + . $processedField['auto_increment'] + . $processedField['null'] + . $processedField['unique']; } /** @@ -246,7 +254,6 @@ protected function _attributeType(array &$attributes) $attributes['TYPE'] = 'NUMBER'; $attributes['CONSTRAINT'] = 1; $attributes['UNSIGNED'] = true; - $attributes['NULL'] = false; return; diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 3e3ed68a9356..3749d8383237 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -146,7 +146,7 @@ public function replace(?array $set = null) $this->set($set); } - if (! $this->QBSet) { + if ($this->QBSet === []) { if ($this->db->DBDebug) { throw new DatabaseException('You must use the "set" method to update an entry.'); } @@ -312,6 +312,132 @@ public function join(string $table, $cond, string $type = '', ?bool $escape = nu return parent::join($table, $cond, $type, $escape); } + /** + * Generates a platform-specific batch update string from the supplied data + * + * @used-by batchExecute + * + * @param string $table Protected table name + * @param list $keys QBKeys + * @param list> $values QBSet + */ + protected function _updateBatch(string $table, array $keys, array $values): string + { + $sql = $this->QBOptions['sql'] ?? ''; + + // if this is the first iteration of batch then we need to build skeleton sql + if ($sql === '') { + $constraints = $this->QBOptions['constraints'] ?? []; + + if ($constraints === []) { + if ($this->db->DBDebug) { + throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore + } + + return ''; // @codeCoverageIgnore + } + + $updateFields = $this->QBOptions['updateFields'] ?? + $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? + []; + + $alias = $this->QBOptions['alias'] ?? '_u'; + + $sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n"; + + $sql .= "SET\n"; + + $that = $this; + $sql .= implode( + ",\n", + array_map( + static fn ($key, $value) => $key . ($value instanceof RawSql ? + ' = ' . $value : + ' = ' . $that->cast($alias . '.' . $value, $that->getFieldType($table, $key))), + array_keys($updateFields), + $updateFields + ) + ) . "\n"; + + $sql .= "FROM (\n{:_table_:}"; + + $sql .= ') ' . $alias . "\n"; + + $sql .= 'WHERE ' . implode( + ' AND ', + array_map( + static function ($key, $value) use ($table, $alias, $that) { + if ($value instanceof RawSql && is_string($key)) { + return $table . '.' . $key . ' = ' . $value; + } + + if ($value instanceof RawSql) { + return $value; + } + + return $table . '.' . $value . ' = ' + . $that->cast($alias . '.' . $value, $that->getFieldType($table, $value)); + }, + array_keys($constraints), + $constraints + ) + ); + + $this->QBOptions['sql'] = $sql; + } + + if (isset($this->QBOptions['setQueryAsData'])) { + $data = $this->QBOptions['setQueryAsData']; + } else { + $data = implode( + " UNION ALL\n", + array_map( + static fn ($value) => 'SELECT ' . implode(', ', array_map( + static fn ($key, $index) => $index . ' ' . $key, + $keys, + $value + )), + $values + ) + ) . "\n"; + } + + return str_replace('{:_table_:}', $data, $sql); + } + + /** + * Returns cast expression. + * + * @TODO move this to BaseBuilder in 4.5.0 + * + * @param float|int|string $expression + */ + private function cast($expression, ?string $type): string + { + return ($type === null) ? $expression : 'CAST(' . $expression . ' AS ' . strtoupper($type) . ')'; + } + + /** + * Returns the filed type from database meta data. + * + * @param string $table Protected table name. + * @param string $fieldName Field name. May be protected. + */ + private function getFieldType(string $table, string $fieldName): ?string + { + $fieldName = trim($fieldName, $this->db->escapeChar); + + if (! isset($this->QBOptions['fieldTypes'][$table])) { + $this->QBOptions['fieldTypes'][$table] = []; + + foreach ($this->db->getFieldData($table) as $field) { + $this->QBOptions['fieldTypes'][$table][$field->name] = $field->type; + } + } + + return $this->QBOptions['fieldTypes'][$table][$fieldName] ?? null; + } + /** * Generates a platform-specific upsertBatch string from the supplied data * @@ -436,18 +562,25 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri $sql .= ') ' . $alias . "\n"; + $that = $this; $sql .= 'WHERE ' . implode( ' AND ', array_map( - static fn ($key, $value) => ( - $value instanceof RawSql ? - $value : - ( - is_string($key) ? - $table . '.' . $key . ' = ' . $alias . '.' . $value : - $table . '.' . $value . ' = ' . $alias . '.' . $value - ) - ), + static function ($key, $value) use ($table, $alias, $that) { + if ($value instanceof RawSql) { + return $value; + } + + if (is_string($key)) { + return $table . '.' . $key . ' = ' + . $that->cast( + $alias . '.' . $value, + $that->getFieldType($table, $key) + ); + } + + return $table . '.' . $value . ' = ' . $alias . '.' . $value; + }, array_keys($constraints), $constraints ) diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index 47be1b063c8d..ee7583642684 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -81,50 +81,52 @@ protected function _createTableAttributes(array $attributes): string } /** - * @param array|string $field + * @param array|string $processedFields Processed column definitions + * or column names to DROP * - * @return array|bool|string + * @return false|list|string SQL string or false + * @phpstan-return ($alterType is 'DROP' ? string : list|false) */ - protected function _alterTable(string $alterType, string $table, $field) + protected function _alterTable(string $alterType, string $table, $processedFields) { if (in_array($alterType, ['DROP', 'ADD'], true)) { - return parent::_alterTable($alterType, $table, $field); + return parent::_alterTable($alterType, $table, $processedFields); } $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); $sqls = []; - foreach ($field as $data) { - if ($data['_literal'] !== false) { + foreach ($processedFields as $field) { + if ($field['_literal'] !== false) { return false; } - if (version_compare($this->db->getVersion(), '8', '>=') && isset($data['type'])) { - $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name']) - . " TYPE {$data['type']}{$data['length']}"; + if (version_compare($this->db->getVersion(), '8', '>=') && isset($field['type'])) { + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) + . " TYPE {$field['type']}{$field['length']}"; } - if (! empty($data['default'])) { - $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name']) - . " SET DEFAULT {$data['default']}"; + if (! empty($field['default'])) { + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) + . " SET DEFAULT {$field['default']}"; } $nullable = true; // Nullable by default. - if (isset($data['null']) && ($data['null'] === false || $data['null'] === ' NOT ' . $this->null)) { + if (isset($field['null']) && ($field['null'] === false || $field['null'] === ' NOT ' . $this->null)) { $nullable = false; } - $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name']) + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) . ($nullable === true ? ' DROP' : ' SET') . ' NOT NULL'; - if (! empty($data['new_name'])) { - $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name']) - . ' TO ' . $this->db->escapeIdentifiers($data['new_name']); + if (! empty($field['new_name'])) { + $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field['name']) + . ' TO ' . $this->db->escapeIdentifiers($field['new_name']); } - if (! empty($data['comment'])) { + if (! empty($field['comment'])) { $sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table) - . '.' . $this->db->escapeIdentifiers($data['name']) - . " IS {$data['comment']}"; + . '.' . $this->db->escapeIdentifiers($field['name']) + . " IS {$field['comment']}"; } } @@ -134,14 +136,14 @@ protected function _alterTable(string $alterType, string $table, $field) /** * Process column */ - protected function _processColumn(array $field): string + protected function _processColumn(array $processedField): string { - return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] . ($field['type'] === 'text' ? '' : $field['length']) - . $field['default'] - . $field['null'] - . $field['auto_increment'] - . $field['unique']; + return $this->db->escapeIdentifiers($processedField['name']) + . ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length']) + . $processedField['default'] + . $processedField['null'] + . $processedField['auto_increment'] + . $processedField['unique']; } /** diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index ff1ce2e59bb9..522d5fa02f54 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -126,23 +126,27 @@ protected function _createTableAttributes(array $attributes): string } /** - * @param array|string $field + * @param array|string $processedFields Processed column definitions + * or column names to DROP * - * @return false|string|string[] + * @return false|list|string SQL string or false + * @phpstan-return ($alterType is 'DROP' ? string : list|false) */ - protected function _alterTable(string $alterType, string $table, $field) + protected function _alterTable(string $alterType, string $table, $processedFields) { // Handle DROP here if ($alterType === 'DROP') { + $columnNamesToDrop = $processedFields; + // check if fields are part of any indexes $indexData = $this->db->getIndexData($table); foreach ($indexData as $index) { - if (is_string($field)) { - $field = explode(',', $field); + if (is_string($columnNamesToDrop)) { + $columnNamesToDrop = explode(',', $columnNamesToDrop); } - $fld = array_intersect($field, $index->fields); + $fld = array_intersect($columnNamesToDrop, $index->fields); // Drop index if field is part of an index if ($fld !== []) { @@ -153,7 +157,7 @@ protected function _alterTable(string $alterType, string $table, $field) $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); // Drop default constraints - $fields = implode(',', $this->db->escape((array) $field)); + $fields = implode(',', $this->db->escape((array) $columnNamesToDrop)); $sql = << 'COLUMN [' . trim($item) . ']', (array) $field); + $fields = array_map(static fn ($item) => 'COLUMN [' . trim($item) . ']', (array) $columnNamesToDrop); return $sql . implode(',', $fields); } @@ -181,45 +185,45 @@ protected function _alterTable(string $alterType, string $table, $field) $sqls = []; if ($alterType === 'ADD') { - foreach ($field as $data) { - $sqls[] = $sql . ($data['_literal'] !== false ? $data['_literal'] : $this->_processColumn($data)); + foreach ($processedFields as $field) { + $sqls[] = $sql . ($field['_literal'] !== false ? $field['_literal'] : $this->_processColumn($field)); } return $sqls; } - foreach ($field as $data) { - if ($data['_literal'] !== false) { + foreach ($processedFields as $field) { + if ($field['_literal'] !== false) { return false; } - if (isset($data['type'])) { - $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name']) - . " {$data['type']}{$data['length']}"; + if (isset($field['type'])) { + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) + . " {$field['type']}{$field['length']}"; } - if (! empty($data['default'])) { - $sqls[] = $sql . ' ALTER COLUMN ADD CONSTRAINT ' . $this->db->escapeIdentifiers($data['name']) . '_def' - . " DEFAULT {$data['default']} FOR " . $this->db->escapeIdentifiers($data['name']); + if (! empty($field['default'])) { + $sqls[] = $sql . ' ALTER COLUMN ADD CONSTRAINT ' . $this->db->escapeIdentifiers($field['name']) . '_def' + . " DEFAULT {$field['default']} FOR " . $this->db->escapeIdentifiers($field['name']); } $nullable = true; // Nullable by default. - if (isset($data['null']) && ($data['null'] === false || $data['null'] === ' NOT ' . $this->null)) { + if (isset($field['null']) && ($field['null'] === false || $field['null'] === ' NOT ' . $this->null)) { $nullable = false; } - $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name']) - . " {$data['type']}{$data['length']} " . ($nullable === true ? '' : 'NOT') . ' NULL'; + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) + . " {$field['type']}{$field['length']} " . ($nullable === true ? '' : 'NOT') . ' NULL'; - if (! empty($data['comment'])) { + if (! empty($field['comment'])) { $sqls[] = 'EXEC sys.sp_addextendedproperty ' - . "@name=N'Caption', @value=N'" . $data['comment'] . "' , " + . "@name=N'Caption', @value=N'" . $field['comment'] . "' , " . "@level0type=N'SCHEMA',@level0name=N'" . $this->db->schema . "', " . "@level1type=N'TABLE',@level1name=N'" . $this->db->escapeIdentifiers($table) . "', " - . "@level2type=N'COLUMN',@level2name=N'" . $this->db->escapeIdentifiers($data['name']) . "'"; + . "@level2type=N'COLUMN',@level2name=N'" . $this->db->escapeIdentifiers($field['name']) . "'"; } - if (! empty($data['new_name'])) { - $sqls[] = "EXEC sp_rename '[" . $this->db->schema . '].[' . $table . '].[' . $data['name'] . "]' , '" . $data['new_name'] . "', 'COLUMN';"; + if (! empty($field['new_name'])) { + $sqls[] = "EXEC sp_rename '[" . $this->db->schema . '].[' . $table . '].[' . $field['name'] . "]' , '" . $field['new_name'] . "', 'COLUMN';"; } } @@ -287,16 +291,16 @@ protected function _processIndexes(string $table, bool $asQuery = false): array /** * Process column */ - protected function _processColumn(array $field): string + protected function _processColumn(array $processedField): string { - return $this->db->escapeIdentifiers($field['name']) - . (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name'])) - . ' ' . $field['type'] . ($field['type'] === 'text' ? '' : $field['length']) - . $field['default'] - . $field['null'] - . $field['auto_increment'] + return $this->db->escapeIdentifiers($processedField['name']) + . (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name'])) + . ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length']) + . $processedField['default'] + . $processedField['null'] + . $processedField['auto_increment'] . '' - . $field['unique']; + . $processedField['unique']; } /** diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index b1dcb1dd599b..d7112c61389f 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -109,51 +109,56 @@ public function dropDatabase(string $dbName): bool } /** - * @param array|string $field + * @param array|string $processedFields Processed column definitions + * or column names to DROP * * @return array|string|null + * @return list|string|null SQL string or null + * @phpstan-return ($alterType is 'DROP' ? string : list|null) */ - protected function _alterTable(string $alterType, string $table, $field) + protected function _alterTable(string $alterType, string $table, $processedFields) { switch ($alterType) { case 'DROP': + $columnNamesToDrop = $processedFields; + $sqlTable = new Table($this->db, $this); $sqlTable->fromTable($table) - ->dropColumn($field) + ->dropColumn($columnNamesToDrop) ->run(); - return ''; + return ''; // Why empty string? case 'CHANGE': (new Table($this->db, $this)) ->fromTable($table) - ->modifyColumn($field) + ->modifyColumn($processedFields) // @TODO Bug: should be NOT processed fields ->run(); - return null; + return null; // Why null? default: - return parent::_alterTable($alterType, $table, $field); + return parent::_alterTable($alterType, $table, $processedFields); } } /** * Process column */ - protected function _processColumn(array $field): string + protected function _processColumn(array $processedField): string { - if ($field['type'] === 'TEXT' && strpos($field['length'], "('") === 0) { - $field['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($field['name']) - . ' IN ' . $field['length'] . ')'; + if ($processedField['type'] === 'TEXT' && strpos($processedField['length'], "('") === 0) { + $processedField['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($processedField['name']) + . ' IN ' . $processedField['length'] . ')'; } - return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] - . $field['auto_increment'] - . $field['null'] - . $field['unique'] - . $field['default']; + return $this->db->escapeIdentifiers($processedField['name']) + . ' ' . $processedField['type'] + . $processedField['auto_increment'] + . $processedField['null'] + . $processedField['unique'] + . $processedField['default']; } /** @@ -183,8 +188,11 @@ protected function _attributeType(array &$attributes) */ protected function _attributeAutoIncrement(array &$attributes, array &$field) { - if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true - && stripos($field['type'], 'int') !== false) { + if ( + ! empty($attributes['AUTO_INCREMENT']) + && $attributes['AUTO_INCREMENT'] === true + && stripos($field['type'], 'int') !== false + ) { $field['type'] = 'INTEGER PRIMARY KEY'; $field['default'] = ''; $field['null'] = ''; diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index b733e980b30e..620d28464738 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -12,7 +12,7 @@ namespace CodeIgniter\Database\SQLite3; use CodeIgniter\Database\Exceptions\DataException; -use stdclass; +use stdClass; /** * Class Table @@ -28,7 +28,7 @@ class Table /** * All of the fields this table represents. * - * @var array> + * @var array> [name => attributes] */ protected $fields = []; @@ -156,7 +156,7 @@ public function run(): bool /** * Drops columns from the table. * - * @param array|string $columns + * @param list|string $columns Column names to drop. * * @return Table */ @@ -177,14 +177,15 @@ public function dropColumn($columns) } /** - * Modifies a field, including changing data type, - * renaming, etc. + * Modifies a field, including changing data type, renaming, etc. + * + * @param list> $fieldsToModify * * @return Table */ - public function modifyColumn(array $fields) + public function modifyColumn(array $fieldsToModify) { - foreach ($fields as $field) { + foreach ($fieldsToModify as $field) { $oldName = $field['name']; unset($field['name']); diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index 33dd126ff1ec..6a5b5e47e5c7 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -182,8 +182,15 @@ protected static function highlightFile(string $file, int $lineNumber, int $line $source = str_replace(["\r\n", "\r"], "\n", $source); $source = explode("\n", highlight_string($source, true)); - $source = str_replace('
', "\n", $source[1]); - $source = explode("\n", str_replace("\r\n", "\n", $source)); + + if (PHP_VERSION_ID < 80300) { + $source = str_replace('
', "\n", $source[1]); + $source = explode("\n", str_replace("\r\n", "\n", $source)); + } else { + // We have to remove these tags since we're preparing the result + // ourselves and these tags are added manually at the end. + $source = str_replace(['
', '
'], '', $source); + } // Get just the part to show $start = max($lineNumber - (int) round($lines / 2), 0); @@ -199,7 +206,7 @@ protected static function highlightFile(string $file, int $lineNumber, int $line // of open and close span tags on one line, we need // to ensure we can close them all to get the lines // showing correctly. - $spans = 1; + $spans = 0; foreach ($source as $n => $row) { $spans += substr_count($row, '' . $format . ' %s', $n + $start + 1, $row) . "\n"; + // We're closing only one span tag we added manually line before, + // so we have to increment $spans count to close this tag later. + $spans++; } } diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index aadf72ee19ad..3652a536443c 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -1,21 +1,21 @@