diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 2bb38f0f1ab2..754b43b8df99 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -29,8 +29,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -40,8 +38,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -74,8 +75,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -85,8 +84,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -119,8 +121,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -130,8 +130,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -165,8 +168,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -176,8 +177,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -209,8 +213,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -220,8 +222,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index fe4b95f2f4b3..891cb7d1d10b 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -19,8 +19,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -30,8 +28,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 18b32b3261a9..2aa858fb68e0 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -8,5 +8,5 @@ permissions: pull-requests: write jobs: - uneditable: + pull-requests: uses: laravel/.github/.github/workflows/pull-requests.yml@main diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index 68e41cfa2aca..1856f6b5b194 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -19,8 +19,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -30,8 +28,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -53,8 +54,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -64,8 +63,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -95,8 +97,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -106,8 +106,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -126,12 +129,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/checkout@v3 + - name: Download & Extract beanstalkd run: curl -L https://github.com/beanstalkd/beanstalkd/archive/refs/tags/v1.13.tar.gz | tar xz + - name: Make beanstalkd run: make working-directory: beanstalkd-1.13 @@ -144,8 +145,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml new file mode 100644 index 000000000000..c4b0cf5490ee --- /dev/null +++ b/.github/workflows/releases.yml @@ -0,0 +1,138 @@ +name: manual release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release' + required: true + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + name: Release ${{ inputs.version }} + + outputs: + version: ${{ steps.version.outputs.version }} + notes: ${{ steps.cleaned-notes.outputs.notes }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Remove optional "v" prefix + id: version + run: | + echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT" + env: + VERSION: ${{ inputs.version }} + + - name: Check if branch and version match + id: guard + run: | + MAJOR_VERSION="${NUMERIC_VERSION%%.*}" + BRANCH_MAJOR_VERSION="${BRANCH%%.*}" + + if [ "$MAJOR_VERSION" != "$BRANCH_MAJOR_VERSION" ]; then + echo "Mismatched versions! Aborting." + VERSION_MISMATCH='true'; + else + echo "Versions match! Proceeding." + VERSION_MISMATCH='false'; + fi + + echo "VERSION_MISMATCH=$(echo $VERSION_MISMATCH)" >> "$GITHUB_OUTPUT"; + env: + BRANCH: ${{ github.ref_name }} + NUMERIC_VERSION: ${{ steps.version.outputs.version }} + + - name: Fail if branch and release tag do not match + if: ${{ steps.guard.outputs.VERSION_MISMATCH == 'true' }} + uses: actions/github-script@v7 + with: + script: | + core.setFailed('Workflow failed. Release version does not match with selected target branch. Did you select the correct branch?') + + - name: Update Application.php version + run: sed -i "s/const VERSION = '.*';/const VERSION = '${{ steps.version.outputs.version }}';/g" src/Illuminate/Foundation/Application.php + + - name: Commit version change + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update version to v${{ steps.version.outputs.version }}" + + - name: SSH into splitter server + uses: appleboy/ssh-action@master + with: + host: 104.248.56.26 + username: forge + key: ${{ secrets.SSH_PRIVATE_KEY_SPLITTER }} + script: | + cd laravel-${{ github.ref_name }} + git pull origin ${{ github.ref_name }} + bash ./bin/release.sh v${{ steps.version.outputs.version }} + script_stop: true + + - name: Generate release notes + id: generated-notes + uses: RedCrafter07/release-notes-action@main + with: + tag-name: v${{ steps.version.outputs.version }} + token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref_name }} + + - name: Cleanup release notes + id: cleaned-notes + run: | + START_FROM=$(echo -n "$RELEASE_NOTES" | awk "/What's Changed/{ print NR; exit }" -) + DROP_FROM_CONTRIBUTORS=$(echo -n "$RELEASE_NOTES" | awk "/New Contributors/{ print NR; exit }" -) + DROP_FROM_FULL_CHANGELOG=$(echo -n "$RELEASE_NOTES" | awk "/Full Changelog/{ print NR; exit }" -) + + # Drop everything starting from "Full Changelog" + if [ ! -z "$DROP_FROM_FULL_CHANGELOG" ]; then + RELEASE_NOTES=$(echo -n "$RELEASE_NOTES" | sed "${DROP_FROM_FULL_CHANGELOG},$ d") + fi + + # Drop everything starting from "New Contributors" + if [ ! -z "$DROP_FROM_CONTRIBUTORS" ]; then + RELEASE_NOTES=$(echo -n "$RELEASE_NOTES" | sed "${DROP_FROM_CONTRIBUTORS},$ d") + fi + + # Drop the line "What's Changed" + if [ ! -z "$START_FROM" ]; then + RELEASE_NOTES=$(echo -n "$RELEASE_NOTES" | sed "${START_FROM}d") + fi + + { + echo 'notes<> "$GITHUB_OUTPUT"; + env: + RELEASE_NOTES: ${{ steps.generated-notes.outputs.release-notes }} + + - name: Create release + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.version }} + name: v${{ steps.version.outputs.version }} + body: ${{ steps.cleaned-notes.outputs.notes }} + target_commitish: ${{ github.ref_name }} + make_latest: "${{ github.ref_name == github.event.repository.default_branch }}" + + update-changelog: + needs: release + + name: Update changelog + + uses: laravel/.github/.github/workflows/update-changelog.yml@main + with: + branch: ${{ github.ref_name }} + version: "v${{ needs.release.outputs.version }}" + notes: ${{ needs.release.outputs.notes }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index d7f42b4641d8..9fb6fdfcf74f 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -21,8 +21,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -31,8 +29,11 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5cda208db8ec..079f71a40337 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,8 +47,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -62,15 +60,18 @@ jobs: REDIS_CONFIGURE_OPTS: --enable-redis --enable-redis-igbinary --enable-redis-msgpack --enable-redis-lzf --with-liblzf --enable-redis-zstd --with-libzstd --enable-redis-lz4 --with-liblz4 REDIS_LIBS: liblz4-dev, liblzf-dev, libzstd-dev - - name: Set Minimum PHP 8.1 Versions - uses: nick-fields/retry@v2 + - name: Set Framework version + run: composer config version "10.x-dev" + + - name: Set minimum PHP 8.1 versions + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: composer require symfony/css-selector:^6.0 --no-interaction --no-update - - name: Set Minimum PHP 8.2 Versions - uses: nick-fields/retry@v2 + - name: Set minimum PHP 8.2 versions + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -78,7 +79,7 @@ jobs: if: matrix.php >= 8.2 - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -95,9 +96,9 @@ jobs: AWS_SECRET_ACCESS_KEY: randomSecret - name: Store artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: logs + name: linux-logs-${{ matrix.php }}-${{ matrix.stability }} path: | vendor/orchestra/testbench-core/laravel/storage/logs !vendor/**/.gitignore @@ -121,8 +122,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -132,15 +131,18 @@ jobs: tools: composer:v2 coverage: none + - name: Set Framework version + run: composer config version "10.x-dev" + - name: Set Minimum PHP 8.1 Versions - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 command: composer require symfony/css-selector:~6.0 --no-interaction --no-update - name: Set Minimum PHP 8.2 Versions - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -148,7 +150,7 @@ jobs: if: matrix.php >= 8.2 - name: Install dependencies - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 @@ -161,9 +163,9 @@ jobs: AWS_SECRET_ACCESS_KEY: random_secret - name: Store artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: logs + name: windows-logs-${{ matrix.php }}-${{ matrix.stability }} path: | vendor/orchestra/testbench-core/laravel/storage/logs !vendor/**/.gitignore diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml deleted file mode 100644 index 1625bda1002c..000000000000 --- a/.github/workflows/update-changelog.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: update changelog - -on: - release: - types: [released] - -jobs: - update: - uses: laravel/.github/.github/workflows/update-changelog.yml@main diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ae112019c8..c1721828ee90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,279 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.40.0...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.48.20...10.x) + +## [v10.48.20](https://github.com/laravel/framework/compare/v10.48.19...v10.48.20) - 2024-08-09 + +* [10.x] fix: prevent casting empty string to array from triggering json error by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/52415 + +## [v10.48.19](https://github.com/laravel/framework/compare/v10.48.18...v10.48.19) - 2024-08-06 + +* Add compatible query type to `Model::resolveRouteBindingQuery` by [@sebj54](https://github.com/sebj54) in https://github.com/laravel/framework/pull/52339 +* [10.x] Fix `Factory::afterCreating` callable argument type by [@villfa](https://github.com/villfa) in https://github.com/laravel/framework/pull/52335 +* [10.x] backport #52204 by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/52389 +* [10.x] In MySQL, harvest last insert ID immediately after query is executed by [@piurafunk](https://github.com/piurafunk) in https://github.com/laravel/framework/pull/52390 + +## [v10.48.18](https://github.com/laravel/framework/compare/v10.48.17...v10.48.18) - 2024-07-30 + +* [10.x] backport #52188 by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/52293 +* [10.x] Fix runPaginationCountQuery not working properly for union queries by [@chinleung](https://github.com/chinleung) in https://github.com/laravel/framework/pull/52314 + +## [v10.48.17](https://github.com/laravel/framework/compare/v10.48.16...v10.48.17) - 2024-07-23 + +* [10.x] Fix PHP_CLI_SERVER_WORKERS warning by suppressing it by [@pelomedusa](https://github.com/pelomedusa) in https://github.com/laravel/framework/pull/52094 +* [10.x] Backport #51615 by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/52215 + +## [v10.48.16](https://github.com/laravel/framework/compare/v10.48.15...v10.48.16) - 2024-07-09 + +* [10.x] Fix Http::retry so that throw is respected for call signature Http::retry([1,2], throw: false) by [@paulyoungnb](https://github.com/paulyoungnb) in https://github.com/laravel/framework/pull/52002 +* [10.x] Set application_name and character set as PostgreSQL DSN string by [@sunaoka](https://github.com/sunaoka) in https://github.com/laravel/framework/pull/51985 + +## [v10.48.15](https://github.com/laravel/framework/compare/v10.48.14...v10.48.15) - 2024-07-02 + +* [10.x] Set previous exception on `HttpResponseException` by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/51986 + +## [v10.48.14](https://github.com/laravel/framework/compare/v10.48.13...v10.48.14) - 2024-06-21 + +* [10.x] Fixes unable to call another command as a initialized instance of `Command` class by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/51824 +* [10.x] fix handle `shift()` on an empty collection by [@Treggats](https://github.com/Treggats) in https://github.com/laravel/framework/pull/51841 +* [10.x] Ensure`schema:dump` will dump the migrations table only if it exists by [@NickSdot](https://github.com/NickSdot) in https://github.com/laravel/framework/pull/51827 + +## [v10.48.13](https://github.com/laravel/framework/compare/v10.48.12...v10.48.13) - 2024-06-18 + +* [10.x] Fix typo in return comment of createSesTransport method by [@zds-s](https://github.com/zds-s) in https://github.com/laravel/framework/pull/51688 +* [10.x] Fix collection shift less than one item by [@faissaloux](https://github.com/faissaloux) in https://github.com/laravel/framework/pull/51686 +* [10.x] Turn `Enumerable unless()` $callback parameter optional by [@faissaloux](https://github.com/faissaloux) in https://github.com/laravel/framework/pull/51701 +* Revert "[10.x] Turn `Enumerable unless()` $callback parameter optional" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/51707 + +## [v10.48.12](https://github.com/laravel/framework/compare/v10.48.11...v10.48.12) - 2024-05-28 + +* [10.x] Fix typo by [@Issei0804-ie](https://github.com/Issei0804-ie) in https://github.com/laravel/framework/pull/51535 +* [10.x] Fix SQL Server detection in database store by [@staudenmeir](https://github.com/staudenmeir) in https://github.com/laravel/framework/pull/51547 +* [10.x] - Fix batch list loading in Horizon when serialization error by [@jeffortegad](https://github.com/jeffortegad) in https://github.com/laravel/framework/pull/51551 +* [10.x] Fixes explicit route binding with `BackedEnum` by [@CAAHS](https://github.com/CAAHS) in https://github.com/laravel/framework/pull/51586 + +## [v10.48.11](https://github.com/laravel/framework/compare/v10.48.10...v10.48.11) - 2024-05-21 + +* [10.x] Backport: Fix SesV2Transport to use correct `EmailTags` argument by [@Tietew](https://github.com/Tietew) in https://github.com/laravel/framework/pull/51352 +* [10.x] Fix PHPDoc typo by [@staudenmeir](https://github.com/staudenmeir) in https://github.com/laravel/framework/pull/51390 +* [10.x] Fix `apa` on non ASCII characters by [@faissaloux](https://github.com/faissaloux) in https://github.com/laravel/framework/pull/51428 +* [10.x] Fixes view engine resolvers leaking memory by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/51450 +* [10.x] Do not use `app()` Foundation helper on `ViewServiceProvider` by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/51522 + +## [v10.48.10](https://github.com/laravel/framework/compare/v10.48.9...v10.48.10) - 2024-04-30 + +* [10.x] Fix typo in signed URL tampering tests by @Krisell in https://github.com/laravel/framework/pull/51238 +* [10.x] Add "Server has gone away" to DetectsLostConnection by @Jubeki in https://github.com/laravel/framework/pull/51241 +* [10.x] Fix support for the LARAVEL_STORAGE_PATH env var (#51238) by @dunglas in https://github.com/laravel/framework/pull/51243 + +## [v10.48.9](https://github.com/laravel/framework/compare/v10.48.8...v10.48.9) - 2024-04-23 + +* [10.x] Binding order is incorrect when using cursor paginate with multiple unions with a where by [@thijsvdanker](https://github.com/thijsvdanker) in https://github.com/laravel/framework/pull/50884 +* [10.x] Fix cursor paginate with union and column alias by [@thijsvdanker](https://github.com/thijsvdanker) in https://github.com/laravel/framework/pull/50882 +* [10.x] Address Null Parameter Deprecations in UrlGenerator by [@aldobarr](https://github.com/aldobarr) in https://github.com/laravel/framework/pull/51148 + +## [v10.48.8](https://github.com/laravel/framework/compare/v10.48.7...v10.48.8) - 2024-04-17 + +* [10.x] Fix error when using `orderByRaw()` in query before using `cursorPaginate()` by @axlon in https://github.com/laravel/framework/pull/51023 +* [10.x] Database layer fixes by @saadsidqui in https://github.com/laravel/framework/pull/49787 + +## [v10.48.7](https://github.com/laravel/framework/compare/v10.48.6...v10.48.7) - 2024-04-10 + +* Fix more query builder methods by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/commit/95ef230339b15321493a08327f250c0760c95376 + +## [v10.48.6](https://github.com/laravel/framework/compare/v10.48.5...v10.48.6) - 2024-04-10 + +* [10.x] Added eachById and chunkByIdDesc to BelongsToMany by [@lonnylot](https://github.com/lonnylot) in https://github.com/laravel/framework/pull/50991 + +## [v10.48.5](https://github.com/laravel/framework/compare/v10.48.4...v10.48.5) - 2024-04-09 + +* [10.x] Prevent Redis connection error report flood on queue worker by [@kasus](https://github.com/kasus) in https://github.com/laravel/framework/pull/50812 +* [10.x] Laravel 10x optional withSize for hasTable by [@apspan](https://github.com/apspan) in https://github.com/laravel/framework/pull/50888 +* [10.x] Add `serializeAndRestore()` to `NotificationFake` by [@dbpolito](https://github.com/dbpolito) in https://github.com/laravel/framework/pull/50935 + +## [v10.48.4](https://github.com/laravel/framework/compare/v10.48.3...v10.48.4) - 2024-03-21 + +* [10.x] Fix `Collection::concat()` return type by @axlon in https://github.com/laravel/framework/pull/50669 +* [10.x] Fix command alias registration and usage by @crynobone in https://github.com/laravel/framework/pull/50695 + +## [v10.48.3](https://github.com/laravel/framework/compare/v10.48.2...v10.48.3) - 2024-03-15 + +- Re-tag version + +## [v10.48.2](https://github.com/laravel/framework/compare/v10.48.1...v10.48.2) - 2024-03-12 + +* [10.x] Update mockery conflict to just disallow the broken version by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/50472 +* [10.x] Conflict with specific release by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50473 +* [10.x] Fix for attributes being escaped on Dynamic Blade Components by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/laravel/framework/pull/50471 +* [10.x] Revert PR 50403 by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50482 + +## [v10.48.1](https://github.com/laravel/framework/compare/v10.48.0...v10.48.1) - 2024-03-12 + +* [10.x] Add conflict for Mockery v1.6.8 by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50468 + +## [v10.48.0](https://github.com/laravel/framework/compare/v10.47.0...v10.48.0) - 2024-03-12 + +* fix: allow null, string and string array as allowed tags by [@maartenpaauw](https://github.com/maartenpaauw) in https://github.com/laravel/framework/pull/50409 +* [10.x] Allow `Expression` at more places in Query Builder by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/laravel/framework/pull/50402 +* [10.x] Sleep syncing by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/50392 +* [10.x] Cleaning Trait on multi-lines by [@gcazin](https://github.com/gcazin) in https://github.com/laravel/framework/pull/50413 +* fix: incomplete type for Builder::from property by [@sebj54](https://github.com/sebj54) in https://github.com/laravel/framework/pull/50426 +* [10.x] After commit callback throwing an exception causes broken transactions afterwards by [@oprypkhantc](https://github.com/oprypkhantc) in https://github.com/laravel/framework/pull/50423 +* [10.x] Anonymous component bound attribute values are evaluated twice by [@danharrin](https://github.com/danharrin) in https://github.com/laravel/framework/pull/50403 +* [10.x] Fix for sortByDesc ignoring multiple attributes by [@TWithers](https://github.com/TWithers) in https://github.com/laravel/framework/pull/50431 +* [10.x] Allow sync with carbon to be set from fake method by [@abenerd](https://github.com/abenerd) in https://github.com/laravel/framework/pull/50450 +* [10.x] Improves `Illuminate\Mail\Mailables\Envelope` docblock by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/50448 +* [10.x] Incorrect return in `FileSystem.php` by [@gcazin](https://github.com/gcazin) in https://github.com/laravel/framework/pull/50459 +* [10.x] fix return types by [@imahmood](https://github.com/imahmood) in https://github.com/laravel/framework/pull/50461 +* fix: phpstan issue - right side of || always false by [@Carnicero90](https://github.com/Carnicero90) in https://github.com/laravel/framework/pull/50453 + +## [v10.47.0](https://github.com/laravel/framework/compare/v10.46.0...v10.47.0) - 2024-03-05 + +* [10.x] Allow for relation key to be an enum by [@AJenbo](https://github.com/AJenbo) in https://github.com/laravel/framework/pull/50311 +* FIx for "empty" strings passed to Str::apa() by [@tiagof](https://github.com/tiagof) in https://github.com/laravel/framework/pull/50335 +* [10.x] Fixed header mail text component to not use markdown by [@dmyers](https://github.com/dmyers) in https://github.com/laravel/framework/pull/50332 +* [10.x] Add test for the "empty strings in `Str::apa()`" fix by [@osbre](https://github.com/osbre) in https://github.com/laravel/framework/pull/50340 +* [10.x] Fix the cache cannot expire cache with `0` TTL by [@kayw-geek](https://github.com/kayw-geek) in https://github.com/laravel/framework/pull/50359 +* [10.x] Add fail on timeout to queue listener by [@saeedhosseiinii](https://github.com/saeedhosseiinii) in https://github.com/laravel/framework/pull/50352 +* [10.x] Support sort option flags on sortByMany Collections by [@TWithers](https://github.com/TWithers) in https://github.com/laravel/framework/pull/50269 +* [10.x] Add `whereAll` and `whereAny` methods to the query builder by [@musiermoore](https://github.com/musiermoore) in https://github.com/laravel/framework/pull/50344 +* [10.x] Adds Reverb broadcasting driver by [@joedixon](https://github.com/joedixon) in https://github.com/laravel/framework/pull/50088 + +## [v10.46.0](https://github.com/laravel/framework/compare/v10.45.1...v10.46.0) - 2024-02-27 + +* [10.x] Ensure lazy-loading for trashed morphTo relations works by [@nuernbergerA](https://github.com/nuernbergerA) in https://github.com/laravel/framework/pull/50176 +* [10.x] Arr::select not working when $keys is a string by [@Sicklou](https://github.com/Sicklou) in https://github.com/laravel/framework/pull/50169 +* [10.x] Added passing loaded relationship to value callback by [@dkulyk](https://github.com/dkulyk) in https://github.com/laravel/framework/pull/50167 +* [10.x] Fix optional charset and collation when creating database by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/50168 +* [10.x] update doc block in PendingProcess.php by [@saMahmoudzadeh](https://github.com/saMahmoudzadeh) in https://github.com/laravel/framework/pull/50198 +* [10.x] Fix Accepting nullable Parameters, updated doc block, and null pointer exception handling in batchable trait by [@saMahmoudzadeh](https://github.com/saMahmoudzadeh) in https://github.com/laravel/framework/pull/50209 +* Make GuardsAttributes fillable property DocBlock more specific by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/50229 +* [10.x] Add only and except methods to Enum validation rule by [@Anton5360](https://github.com/Anton5360) in https://github.com/laravel/framework/pull/50226 +* [10.x] Fixes on nesting operations performed while applying scopes. by [@Guilhem-DELAITRE](https://github.com/Guilhem-DELAITRE) in https://github.com/laravel/framework/pull/50207 +* [10.x] Custom RateLimiter increase by [@khepin](https://github.com/khepin) in https://github.com/laravel/framework/pull/50197 +* [10.x] Add Lateral Join to Query Builder by [@Bakke](https://github.com/Bakke) in https://github.com/laravel/framework/pull/50050 +* [10.x] Update return type by [@AmirRezaM75](https://github.com/AmirRezaM75) in https://github.com/laravel/framework/pull/50252 +* [10.x] Fix dockblock by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/50259 +* [10.x] Add `Conditionable` in enum rule by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/50257 +* [10.x] Update Facade::$app to nullable by [@villfa](https://github.com/villfa) in https://github.com/laravel/framework/pull/50260 +* [10.x] Truncate sqlite table name with prefix by [@kitloong](https://github.com/kitloong) in https://github.com/laravel/framework/pull/50251 +* Correction comment for Str::orderedUuid() - https://github.com/larave… by [@wq9578](https://github.com/wq9578) in https://github.com/laravel/framework/pull/50268 + +## [v10.45.1](https://github.com/laravel/framework/compare/v10.45.0...v10.45.1) - 2024-02-21 + +* Fix typehint for ResetPassword::toMailUsing() by [@KKSzymanowski](https://github.com/KKSzymanowski) in https://github.com/laravel/framework/pull/50163 +* [10.x] Fix Process::fake() never matching multi-line commands by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/50164 + +## [v10.45.0](https://github.com/laravel/framework/compare/v10.44.0...v10.45.0) - 2024-02-20 + +* [10.x] Update `Stringable` phpdoc by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/50075 +* [10.x] Allow `Collection::select()` to work on `ArrayAccess` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/50072 +* [10.x] Add `before` to the `PendingBatch` by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/50058 +* [10.x] Adjust rules call sequence by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50084 +* [10.x] Fixes `Illuminate\Support\Str::fromBase64()` return type by [@SamAsEnd](https://github.com/SamAsEnd) in https://github.com/laravel/framework/pull/50108 +* [10.x] Actually fix fromBase64 return type by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/50113 +* [10.x] Fix warning and deprecation for Str::api by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/50114 +* [10.x] Mark model instanse as not exists on deleting MorphPivot relation. by [@dkulyk](https://github.com/dkulyk) in https://github.com/laravel/framework/pull/50135 +* [10.x] Adds Tappable and Conditionable to Relation class by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/50124 +* [10.x] Added getQualifiedMorphTypeName to MorphToMany by [@dkulyk](https://github.com/dkulyk) in https://github.com/laravel/framework/pull/50153 + +## [v10.44.0](https://github.com/laravel/framework/compare/v10.43.0...v10.44.0) - 2024-02-13 + +* [10.x] Fix empty request for HTTP connection exception by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/49924 +* [10.x] Add Collection::select() method by [@morrislaptop](https://github.com/morrislaptop) in https://github.com/laravel/framework/pull/49845 +* [10.x] Refactor `getPreviousUrlFromSession` method in UrlGenerator by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/49944 +* [10.x] Add POSIX compliant cleanup to artisan serve by [@Tofandel](https://github.com/Tofandel) in https://github.com/laravel/framework/pull/49943 +* [10.x] Fix infinite loop when global scopes query contains aggregates by [@mateusjunges](https://github.com/mateusjunges) in https://github.com/laravel/framework/pull/49972 +* [10.x] Adds PHPUnit 11 as conflict by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/49957 +* Revert "[10.x] fix Before/After validation rules" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/50013 +* [10.x] Fix the phpdoc for replaceMatches in Str and Stringable helpers by [@joke2k](https://github.com/joke2k) in https://github.com/laravel/framework/pull/49990 +* [10.x] Added `setAbly()` method for `AblyBroadcaster` by [@Rijoanul-Shanto](https://github.com/Rijoanul-Shanto) in https://github.com/laravel/framework/pull/49981 +* [10.x] Fix in appendExceptionToException method exception type check by [@t1nkl](https://github.com/t1nkl) in https://github.com/laravel/framework/pull/49958 +* [10.x] DB command: add sqlcmd -C flag when 'trust_server_certificate' is set by [@hulkur](https://github.com/hulkur) in https://github.com/laravel/framework/pull/49952 +* Allows Setup and Teardown actions to be reused in alternative TestCase for Laravel by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49973 +* [10.x] Add `toBase64()` and `fromBase64()` methods to Stringable and Str classes by [@mtownsend5512](https://github.com/mtownsend5512) in https://github.com/laravel/framework/pull/49984 +* [10.x] Allows to defer resolving pcntl only if it's available by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/50024 +* [10.x] Fixes missing `Throwable` import and handle if `originalExceptionHandler` or `originalDeprecationHandler` property isn't used by alternative TestCase by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/50021 +* [10.x] Type hinting for conditional validation rules by [@lorenzolosa](https://github.com/lorenzolosa) in https://github.com/laravel/framework/pull/50017 +* [10.x] Introduce new `Arr::take()` helper by [@ryangjchandler](https://github.com/ryangjchandler) in https://github.com/laravel/framework/pull/50015 +* [10.x] Improved Handling of Empty Component Slots with HTML Comments or Line Breaks by [@comes](https://github.com/comes) in https://github.com/laravel/framework/pull/49966 +* [10.x] Introduce Observe attribute for models by [@emargareten](https://github.com/emargareten) in https://github.com/laravel/framework/pull/49843 +* [10.x] Add ScopedBy attribute for models by [@emargareten](https://github.com/emargareten) in https://github.com/laravel/framework/pull/50034 +* [10.x] Update reserved names in `GeneratorCommand` by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/50043 +* [10.x] fix Validator::validated get nullable array by [@helitik](https://github.com/helitik) in https://github.com/laravel/framework/pull/50056 +* [10.x] Pass Herd specific env variables to "artisan serve" by [@mpociot](https://github.com/mpociot) in https://github.com/laravel/framework/pull/50069 +* Remove regex case insensitivity modifier in UUID detection to speed it up slightly by [@maximal](https://github.com/maximal) in https://github.com/laravel/framework/pull/50067 +* [10.x] HTTP retry method can accept array as first param by [@me-shaon](https://github.com/me-shaon) in https://github.com/laravel/framework/pull/50064 +* [10.x] Fix DB::afterCommit() broken in tests using DatabaseTransactions by [@oprypkhantc](https://github.com/oprypkhantc) in https://github.com/laravel/framework/pull/50068 + +## [v10.43.0](https://github.com/laravel/framework/compare/v10.42.0...v10.43.0) - 2024-01-30 + +* [10.x] Add storage:unlink command by [@salkovmx](https://github.com/salkovmx) in https://github.com/laravel/framework/pull/49795 +* [10.x] Unify `\Illuminate\Log\LogManager` method definition comments with `\Psr\Logger\Interface` by [@eusonlito](https://github.com/eusonlito) in https://github.com/laravel/framework/pull/49805 +* [10.x] class-name string argument for global scopes by [@emargareten](https://github.com/emargareten) in https://github.com/laravel/framework/pull/49802 +* [10.x] Add `hasIndex()` and minor Schema enhancements by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49796 +* [10.x] Do not touch `BelongsToMany` relation when using `withoutTouching` by [@mateusjunges](https://github.com/mateusjunges) in https://github.com/laravel/framework/pull/49798 +* [10.x] Check properties on mailables are initialized before sharing with the view by [@j3j5](https://github.com/j3j5) in https://github.com/laravel/framework/pull/49813 +* [10.x] Remove duplicate actions/checkout from queue workflow by [@Jubeki](https://github.com/Jubeki) in https://github.com/laravel/framework/pull/49828 +* [10.x] Add `insertOrIgnoreUsing` for Eloquent by [@trovster](https://github.com/trovster) in https://github.com/laravel/framework/pull/49827 +* [10.x] Make `hasIndex()` Order-sensitive by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/49840 +* [10.x] Release action by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/49838 +* [10.x] Add MariaDb1060Platform by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/49848 +* [10.x] Unified Pivot and Model Doc Block `$guarded` by [@eusonlito](https://github.com/eusonlito) in https://github.com/laravel/framework/pull/49851 +* [10.x] Introducing `beforeStartingTransaction` callback and use it in `LazilyRefreshDatabase` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/laravel/framework/pull/49853 +* [10.x] fix password max validation message by [@MrPunyapal](https://github.com/MrPunyapal) in https://github.com/laravel/framework/pull/49861 +* [10.x] Fix validation message used for max file size by [@mateusjunges](https://github.com/mateusjunges) in https://github.com/laravel/framework/pull/49879 +* Update README.md by [@foremtehan](https://github.com/foremtehan) in https://github.com/laravel/framework/pull/49878 +* [10.x] Adds `FormRequest[@getRules](https://github.com/getRules)()` method by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/49860 +* [10.x] add addGlobalScopes method by [@emargareten](https://github.com/emargareten) in https://github.com/laravel/framework/pull/49880 +* [10.x] Allow brick/math 0.12 by [@LogicSatinn](https://github.com/LogicSatinn) in https://github.com/laravel/framework/pull/49883 +* [10.x] Add support for streamed JSON Response by [@pelmered](https://github.com/pelmered) in https://github.com/laravel/framework/pull/49873 +* [10.x] Using the native fopen exception in LockableFile.php by [@eusonlito](https://github.com/eusonlito) in https://github.com/laravel/framework/pull/49895 +* [10.x] Fix LazilyRefreshDatabase when testing artisan commands by [@iamgergo](https://github.com/iamgergo) in https://github.com/laravel/framework/pull/49914 +* [10.x] Fix expressions in with-functions doing aggregates by [@tpetry](https://github.com/tpetry) in https://github.com/laravel/framework/pull/49912 +* [10.x] Fix redis tag entries never becoming stale if cache ttl is past time by [@jagers](https://github.com/jagers) in https://github.com/laravel/framework/pull/49864 +* [10.x] Fix - The `Translator` may incorrectly report the locale of a missing translation key by [@VicGUTT](https://github.com/VicGUTT) in https://github.com/laravel/framework/pull/49900 +* [10.x] fix Before/After validation rules by [@MrPunyapal](https://github.com/MrPunyapal) in https://github.com/laravel/framework/pull/49871 + +## [v10.42.0](https://github.com/laravel/framework/compare/v10.41.0...v10.42.0) - 2024-01-23 + +* [10.x] Switch to hash_equals in `File::hasSameHash()` by [@simonhamp](https://github.com/simonhamp) in https://github.com/laravel/framework/pull/49721 +* [10.x] fix Rule::unless for callable $condition by [@dbakan](https://github.com/dbakan) in https://github.com/laravel/framework/pull/49726 +* [10.x] Adds JobQueueing event by [@dmason30](https://github.com/dmason30) in https://github.com/laravel/framework/pull/49722 +* [10.x] Fix decoding issue in MailLogTransport by [@rojtjo](https://github.com/rojtjo) in https://github.com/laravel/framework/pull/49727 +* [10.x] Implement "max" validation rule for passwords by [@angelej](https://github.com/angelej) in https://github.com/laravel/framework/pull/49739 +* [10.x] Add multiple channels/routes to AnonymousNotifiable at once by [@iamgergo](https://github.com/iamgergo) in https://github.com/laravel/framework/pull/49745 +* [10.x] Sort service providers alphabetically by [@buismaarten](https://github.com/buismaarten) in https://github.com/laravel/framework/pull/49762 +* [10.x] Global default options for the http factory by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/49767 +* [10.x] Only use `Carbon` if accessed from Laravel or also uses `illuminate/support` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49772 +* [10.x] Add `Str::unwrap` by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/49779 +* [10.x] Allow Uuid and Ulid in Carbon::createFromId() by [@kylekatarnls](https://github.com/kylekatarnls) in https://github.com/laravel/framework/pull/49783 +* [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49785 + +## [v10.41.0](https://github.com/laravel/framework/compare/v10.40.0...v10.41.0) - 2024-01-16 + +* [10.x] Add a `threshold` parameter to the `Number::spell` helper by [@caendesilva](https://github.com/caendesilva) in https://github.com/laravel/framework/pull/49610 +* Revert "[10.x] Make ComponentAttributeBag Arrayable" by [@luanfreitasdev](https://github.com/luanfreitasdev) in https://github.com/laravel/framework/pull/49623 +* [10.x] Fix return value and docblock by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/49627 +* [10.x] Add an option to specify the default path to the models directory for `php artisan model:prune` by [@dbhynds](https://github.com/dbhynds) in https://github.com/laravel/framework/pull/49617 +* [10.x] Allow job chains to be conditionally dispatched by [@fjarrett](https://github.com/fjarrett) in https://github.com/laravel/framework/pull/49624 +* [10.x] Add test for existing empty test by [@lioneaglesolutions](https://github.com/lioneaglesolutions) in https://github.com/laravel/framework/pull/49632 +* [10.x] Add additional context to Mailable assertion messages by [@lioneaglesolutions](https://github.com/lioneaglesolutions) in https://github.com/laravel/framework/pull/49631 +* [10.x] Allow job batches to be conditionally dispatched by [@fjarrett](https://github.com/fjarrett) in https://github.com/laravel/framework/pull/49639 +* [10.x] Revert parameter name change by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/49659 +* [10.x] Printing Name of The Method that Calls `ensureIntlExtensionIsInstalled` in `Number` class. by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/49660 +* [10.x] Update pagination tailwind.blade.php by [@anasmorahhib](https://github.com/anasmorahhib) in https://github.com/laravel/framework/pull/49665 +* [10.x] feat: add base argument to Stringable->toInteger() by [@adamczykpiotr](https://github.com/adamczykpiotr) in https://github.com/laravel/framework/pull/49670 +* [10.x]: Remove unused class ShouldBeUnique when make a job by [@Kenini1805](https://github.com/Kenini1805) in https://github.com/laravel/framework/pull/49669 +* [10.x] Add tests for Eloquent methods by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/49673 +* Implement draft workflow by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/49683 +* [10.x] Fixing Types, Word and Returns of `Number`class. by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/49681 +* [10.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/49679 +* [10.x] Officially support floats in trans_choice and Translator::choice by [@philbates35](https://github.com/philbates35) in https://github.com/laravel/framework/pull/49693 +* [10.x] Use static function by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/49696 +* [10.x] Revert "[10.x] Improve numeric comparison for custom casts" by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/49702 +* [10.x] Add exit code to queue:clear, and queue:forget commands by [@bytestream](https://github.com/bytestream) in https://github.com/laravel/framework/pull/49707 +* [10.x] Allow StreamInterface as raw HTTP Client body by [@janolivermr](https://github.com/janolivermr) in https://github.com/laravel/framework/pull/49705 ## [v10.40.0](https://github.com/laravel/framework/compare/v10.39.0...v10.40.0) - 2024-01-09 diff --git a/README.md b/README.md index df935e86ac3f..a6fb7790a95a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Laravel has the most extensive and thorough documentation and video tutorial lib You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. -If you're not in the mood to read, [Laracasts](https://laracasts.com) contains over 1100 video tutorials covering a range of topics including Laravel, modern PHP, unit testing, JavaScript, and more. Boost the skill level of yourself and your entire team by digging into our comprehensive video library. +If you're not in the mood to read, [Laracasts](https://laracasts.com) contains thousands of video tutorials covering a range of topics including Laravel, modern PHP, unit testing, JavaScript, and more. Boost the skill level of yourself and your entire team by digging into our comprehensive video library. ## Contributing diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000000..953dc1778d31 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,5 @@ +# Release Instructions + +Go to the ["manual release" GitHub Action](https://github.com/laravel/framework/actions/workflows/releases.yml). Then, choose "Run workflow", select the correct branch, and enter the version you wish to release. Next, press "Run workflow" to execute the action. The workflow will automatically update the version in `Application.php`, tag a new release, run the splitter script for the Illuminate components, generate release notes, create a GitHub Release, and update the `CHANGELOG.md` file. + +Screenshot 2024-05-06 at 10 46 04 diff --git a/composer.json b/composer.json index dac49e5946c7..169bc9942622 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "ext-session": "*", "ext-tokenizer": "*", "composer-runtime-api": "^2.2", - "brick/math": "^0.9.3|^0.10.2|^0.11", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.3.2", "egulias/email-validator": "^3.2.1|^4.0", @@ -105,7 +105,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.18", + "orchestra/testbench-core": "^8.23.4", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.0.7", @@ -121,7 +121,9 @@ "conflict": { "carbonphp/carbon-doctrine-types": ">=3.0", "doctrine/dbal": ">=4.0", - "tightenco/collect": "<5.5.33" + "mockery/mockery": "1.6.8", + "tightenco/collect": "<5.5.33", + "phpunit/phpunit": ">=11.0.0" }, "autoload": { "files": [ diff --git a/src/Illuminate/Auth/Notifications/ResetPassword.php b/src/Illuminate/Auth/Notifications/ResetPassword.php index 1d8da41bd1a8..efb4573e8be2 100644 --- a/src/Illuminate/Auth/Notifications/ResetPassword.php +++ b/src/Illuminate/Auth/Notifications/ResetPassword.php @@ -25,7 +25,7 @@ class ResetPassword extends Notification /** * The callback that should be used to build the mail message. * - * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage)|null + * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable)|null */ public static $toMailCallback; @@ -114,7 +114,7 @@ public static function createUrlUsing($callback) /** * Set a callback that should be used when building the notification mail message. * - * @param \Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage $callback + * @param \Closure(mixed, string): (\Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable) $callback * @return void */ public static function toMailUsing($callback) diff --git a/src/Illuminate/Auth/TokenGuard.php b/src/Illuminate/Auth/TokenGuard.php index b1aa7a7e5162..7fe5a9f7802a 100644 --- a/src/Illuminate/Auth/TokenGuard.php +++ b/src/Illuminate/Auth/TokenGuard.php @@ -92,7 +92,7 @@ public function user() /** * Get the token for the current request. * - * @return string + * @return string|null */ public function getTokenForRequest() { diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index e8bdf0bad873..192cb3b2d639 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -273,6 +273,17 @@ protected function callCustomCreator(array $config) return $this->customCreators[$config['driver']]($this->app, $config); } + /** + * Create an instance of the driver. + * + * @param array $config + * @return \Illuminate\Contracts\Broadcasting\Broadcaster + */ + protected function createReverbDriver(array $config) + { + return $this->createPusherDriver($config); + } + /** * Create an instance of the driver. * diff --git a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php index a1e283a32c96..01c673c22f32 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php @@ -232,4 +232,15 @@ public function getAbly() { return $this->ably; } + + /** + * Set the underlying Ably SDK instance. + * + * @param \Ably\AblyRest $ably + * @return void + */ + public function setAbly($ably) + { + $this->ably = $ably; + } } diff --git a/src/Illuminate/Bus/Batchable.php b/src/Illuminate/Bus/Batchable.php index 0b082700f8a2..5cf5706070e9 100644 --- a/src/Illuminate/Bus/Batchable.php +++ b/src/Illuminate/Bus/Batchable.php @@ -35,7 +35,7 @@ public function batch() } if ($this->batchId) { - return Container::getInstance()->make(BatchRepository::class)->find($this->batchId); + return Container::getInstance()->make(BatchRepository::class)?->find($this->batchId); } } @@ -74,7 +74,7 @@ public function withBatchId(string $batchId) * @param int $failedJobs * @param array $failedJobIds * @param array $options - * @param \Carbon\CarbonImmutable $createdAt + * @param \Carbon\CarbonImmutable|null $createdAt * @param \Carbon\CarbonImmutable|null $cancelledAt * @param \Carbon\CarbonImmutable|null $finishedAt * @return array{0: $this, 1: \Illuminate\Support\Testing\Fakes\BatchFake} @@ -86,7 +86,7 @@ public function withFakeBatch(string $id = '', int $failedJobs = 0, array $failedJobIds = [], array $options = [], - CarbonImmutable $createdAt = null, + ?CarbonImmutable $createdAt = null, ?CarbonImmutable $cancelledAt = null, ?CarbonImmutable $finishedAt = null) { diff --git a/src/Illuminate/Bus/DatabaseBatchRepository.php b/src/Illuminate/Bus/DatabaseBatchRepository.php index 4333c515ac79..5a7aadde66bd 100644 --- a/src/Illuminate/Bus/DatabaseBatchRepository.php +++ b/src/Illuminate/Bus/DatabaseBatchRepository.php @@ -6,10 +6,10 @@ use Closure; use DateTimeInterface; use Illuminate\Database\Connection; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\PostgresConnection; use Illuminate\Database\Query\Expression; use Illuminate\Support\Str; +use Throwable; class DatabaseBatchRepository implements PrunableBatchRepository { @@ -352,7 +352,7 @@ protected function unserialize($serialized) try { return unserialize($serialized); - } catch (ModelNotFoundException) { + } catch (Throwable) { return []; } } diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index b9622f8cc064..1b3b01bd061f 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -74,6 +74,31 @@ public function add($jobs) return $this; } + /** + * Add a callback to be executed when the batch is stored. + * + * @param callable $callback + * @return $this + */ + public function before($callback) + { + $this->options['before'][] = $callback instanceof Closure + ? new SerializableClosure($callback) + : $callback; + + return $this; + } + + /** + * Get the "before" callbacks that have been registered with the pending batch. + * + * @return array + */ + public function beforeCallbacks() + { + return $this->options['before'] ?? []; + } + /** * Add a callback to be executed after a job in the batch have executed successfully. * @@ -282,7 +307,7 @@ public function dispatch() $repository = $this->container->make(BatchRepository::class); try { - $batch = $repository->store($this); + $batch = $this->store($repository); $batch = $batch->add($this->jobs); } catch (Throwable $e) { @@ -309,7 +334,7 @@ public function dispatchAfterResponse() { $repository = $this->container->make(BatchRepository::class); - $batch = $repository->store($this); + $batch = $this->store($repository); if ($batch) { $this->container->terminating(function () use ($batch) { @@ -366,4 +391,27 @@ public function dispatchUnless($boolean) { return ! value($boolean) ? $this->dispatch() : null; } + + /** + * Store the batch using the given repository. + * + * @param \Illuminate\Bus\BatchRepository $repository + * @return \Illuminate\Bus\Batch + */ + protected function store($repository) + { + $batch = $repository->store($this); + + collect($this->beforeCallbacks())->each(function ($handler) use ($batch) { + try { + return $handler($batch); + } catch (Throwable $e) { + if (function_exists('report')) { + report($e); + } + } + }); + + return $batch; + } } diff --git a/src/Illuminate/Cache/Console/PruneStaleTagsCommand.php b/src/Illuminate/Cache/Console/PruneStaleTagsCommand.php index 95665f9bab87..dbb2f6bd0860 100644 --- a/src/Illuminate/Cache/Console/PruneStaleTagsCommand.php +++ b/src/Illuminate/Cache/Console/PruneStaleTagsCommand.php @@ -29,7 +29,7 @@ class PruneStaleTagsCommand extends Command * Execute the console command. * * @param \Illuminate\Cache\CacheManager $cache - * @return void + * @return int|null */ public function handle(CacheManager $cache) { diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index e5b72107ce96..b8026e3e0de9 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -158,9 +158,7 @@ public function add($key, $value, $seconds) $value = $this->serialize($value); $expiration = $this->getTime() + $seconds; - $doesntSupportInsertOrIgnore = [SqlServerConnection::class]; - - if (! in_array(get_class($this->getConnection()), $doesntSupportInsertOrIgnore)) { + if (! $this->getConnection() instanceof SqlServerConnection) { return $this->table()->insertOrIgnore(compact('key', 'value', 'expiration')) > 0; } diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index 5f5fac0659b6..afdb9b25a208 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -105,13 +105,26 @@ public function tooManyAttempts($key, $maxAttempts) } /** - * Increment the counter for a given key for a given decay time. + * Increment (by 1) the counter for a given key for a given decay time. * * @param string $key * @param int $decaySeconds * @return int */ public function hit($key, $decaySeconds = 60) + { + return $this->increment($key, $decaySeconds); + } + + /** + * Increment the counter for a given key for a given decay time by a given amount. + * + * @param string $key + * @param int $decaySeconds + * @param int $amount + * @return int + */ + public function increment($key, $decaySeconds = 60, $amount = 1) { $key = $this->cleanRateLimiterKey($key); @@ -121,7 +134,7 @@ public function hit($key, $decaySeconds = 60) $added = $this->cache->add($key, 0, $decaySeconds); - $hits = (int) $this->cache->increment($key); + $hits = (int) $this->cache->increment($key, $amount); if (! $added && $hits == 1) { $this->cache->put($key, 1, $decaySeconds); diff --git a/src/Illuminate/Cache/RedisTagSet.php b/src/Illuminate/Cache/RedisTagSet.php index bf4c53869361..b5fd0e2593bc 100644 --- a/src/Illuminate/Cache/RedisTagSet.php +++ b/src/Illuminate/Cache/RedisTagSet.php @@ -11,13 +11,13 @@ class RedisTagSet extends TagSet * Add a reference entry to the tag set's underlying sorted set. * * @param string $key - * @param int $ttl + * @param int|null $ttl * @param string $updateWhen * @return void */ - public function addEntry(string $key, int $ttl = 0, $updateWhen = null) + public function addEntry(string $key, int $ttl = null, $updateWhen = null) { - $ttl = $ttl > 0 ? Carbon::now()->addSeconds($ttl)->getTimestamp() : -1; + $ttl = is_null($ttl) ? -1 : Carbon::now()->addSeconds($ttl)->getTimestamp(); foreach ($this->tagIds() as $tagKey) { if ($updateWhen) { diff --git a/src/Illuminate/Cache/RedisTaggedCache.php b/src/Illuminate/Cache/RedisTaggedCache.php index b8120be95c03..8846844b413d 100644 --- a/src/Illuminate/Cache/RedisTaggedCache.php +++ b/src/Illuminate/Cache/RedisTaggedCache.php @@ -14,10 +14,18 @@ class RedisTaggedCache extends TaggedCache */ public function add($key, $value, $ttl = null) { - $this->tags->addEntry( - $this->itemKey($key), - ! is_null($ttl) ? $this->getSeconds($ttl) : 0 - ); + $seconds = null; + + if ($ttl !== null) { + $seconds = $this->getSeconds($ttl); + + if ($seconds > 0) { + $this->tags->addEntry( + $this->itemKey($key), + $seconds + ); + } + } return parent::add($key, $value, $ttl); } @@ -36,10 +44,14 @@ public function put($key, $value, $ttl = null) return $this->forever($key, $value); } - $this->tags->addEntry( - $this->itemKey($key), - $this->getSeconds($ttl) - ); + $seconds = $this->getSeconds($ttl); + + if ($seconds > 0) { + $this->tags->addEntry( + $this->itemKey($key), + $seconds + ); + } return parent::put($key, $value, $ttl); } diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 44f3181f3f4a..606f73b3171c 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -22,8 +22,7 @@ */ class Repository implements ArrayAccess, CacheContract { - use InteractsWithTime; - use Macroable { + use InteractsWithTime, Macroable { __call as macroCall; } diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index d1e4a40ae686..361808418ffa 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -225,6 +225,22 @@ public static function last($array, callable $callback = null, $default = null) return static::first(array_reverse($array, true), $callback, $default); } + /** + * Take the first or last {$limit} items from an array. + * + * @param array $array + * @param int $limit + * @return array + */ + public static function take($array, $limit) + { + if ($limit < 0) { + return array_slice($array, $limit, abs($limit)); + } + + return array_slice($array, 0, $limit); + } + /** * Flatten a multi-dimensional array into a single level. * @@ -491,6 +507,32 @@ public static function only($array, $keys) return array_intersect_key($array, array_flip((array) $keys)); } + /** + * Select an array of values from an array. + * + * @param array $array + * @param array|string $keys + * @return array + */ + public static function select($array, $keys) + { + $keys = static::wrap($keys); + + return static::map($array, function ($item) use ($keys) { + $result = []; + + foreach ($keys as $key) { + if (Arr::accessible($item) && Arr::exists($item, $key)) { + $result[$key] = $item[$key]; + } elseif (is_object($item) && isset($item->{$key})) { + $result[$key] = $item->{$key}; + } + } + + return $result; + }); + } + /** * Pluck an array of values from an array. * diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 7869e1ce65e0..d7369d686591 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString; use Illuminate\Support\Traits\EnumeratesValues; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use stdClass; use Traversable; @@ -918,6 +919,27 @@ public function only($keys) return new static(Arr::only($this->items, $keys)); } + /** + * Select specific values from the items within the collection. + * + * @param \Illuminate\Support\Enumerable|array|string|null $keys + * @return static + */ + public function select($keys) + { + if (is_null($keys)) { + return new static($this->items); + } + + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } + + $keys = is_array($keys) ? $keys : func_get_args(); + + return new static(Arr::select($this->items, $keys)); + } + /** * Get and remove the last N items from the collection. * @@ -977,8 +999,11 @@ public function push(...$values) /** * Push all of the given items onto the collection. * - * @param iterable $source - * @return static + * @template TConcatKey of array-key + * @template TConcatValue + * + * @param iterable $source + * @return static */ public function concat($source) { @@ -1100,17 +1125,27 @@ public function search($value, $strict = false) * * @param int $count * @return static|TValue|null + * + * @throws \InvalidArgumentException */ public function shift($count = 1) { - if ($count === 1) { - return array_shift($this->items); + if ($count < 0) { + throw new InvalidArgumentException('Number of shifted items may not be less than zero.'); } if ($this->isEmpty()) { + return null; + } + + if ($count === 0) { return new static; } + if ($count === 1) { + return array_shift($this->items); + } + $results = []; $collectionCount = $this->count(); @@ -1376,7 +1411,7 @@ public function sortDesc($options = SORT_REGULAR) public function sortBy($callback, $options = SORT_REGULAR, $descending = false) { if (is_array($callback) && ! is_callable($callback)) { - return $this->sortByMany($callback); + return $this->sortByMany($callback, $options); } $results = []; @@ -1407,13 +1442,14 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) * Sort the collection using multiple comparisons. * * @param array $comparisons + * @param int $options * @return static */ - protected function sortByMany(array $comparisons = []) + protected function sortByMany(array $comparisons = [], int $options = SORT_REGULAR) { $items = $this->items; - uasort($items, function ($a, $b) use ($comparisons) { + uasort($items, function ($a, $b) use ($comparisons, $options) { foreach ($comparisons as $comparison) { $comparison = Arr::wrap($comparison); @@ -1431,7 +1467,21 @@ protected function sortByMany(array $comparisons = []) $values = array_reverse($values); } - $result = $values[0] <=> $values[1]; + if (($options & SORT_FLAG_CASE) === SORT_FLAG_CASE) { + if (($options & SORT_NATURAL) === SORT_NATURAL) { + $result = strnatcasecmp($values[0], $values[1]); + } else { + $result = strcasecmp($values[0], $values[1]); + } + } else { + $result = match ($options) { + SORT_NUMERIC => intval($values[0]) <=> intval($values[1]), + SORT_STRING => strcmp($values[0], $values[1]), + SORT_NATURAL => strnatcmp($values[0], $values[1]), + SORT_LOCALE_STRING => strcoll($values[0], $values[1]), + default => $values[0] <=> $values[1], + }; + } } if ($result === 0) { @@ -1454,6 +1504,16 @@ protected function sortByMany(array $comparisons = []) */ public function sortByDesc($callback, $options = SORT_REGULAR) { + if (is_array($callback) && ! is_callable($callback)) { + foreach ($callback as $index => $key) { + $comparison = Arr::wrap($key); + + $comparison[1] = 'desc'; + + $callback[$index] = $comparison; + } + } + return $this->sortBy($callback, $options, true); } diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index a561488e8a59..918f64758e66 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -789,8 +789,11 @@ public function partition($key, $operator = null, $value = null); /** * Push all of the given items onto the collection. * - * @param iterable $source - * @return static + * @template TConcatKey of array-key + * @template TConcatValue + * + * @param iterable $source + * @return static */ public function concat($source); diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index ce84977aad99..84b22ebf9257 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -954,11 +954,49 @@ public function only($keys) } /** - * Push all of the given items onto the collection. + * Select specific values from the items within the collection. * - * @param iterable $source + * @param \Illuminate\Support\Enumerable|array|string $keys * @return static */ + public function select($keys) + { + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } elseif (! is_null($keys)) { + $keys = is_array($keys) ? $keys : func_get_args(); + } + + return new static(function () use ($keys) { + if (is_null($keys)) { + yield from $this; + } else { + foreach ($this as $item) { + $result = []; + + foreach ($keys as $key) { + if (Arr::accessible($item) && Arr::exists($item, $key)) { + $result[$key] = $item[$key]; + } elseif (is_object($item) && isset($item->{$key})) { + $result[$key] = $item->{$key}; + } + } + + yield $result; + } + } + }); + } + + /** + * Push all of the given items onto the collection. + * + * @template TConcatKey of array-key + * @template TConcatValue + * + * @param iterable $source + * @return static + */ public function concat($source) { return (new static(function () use ($source) { @@ -1740,6 +1778,8 @@ protected function passthru($method, array $params) */ protected function now() { - return Carbon::now()->timestamp; + return class_exists(Carbon::class) + ? Carbon::now()->timestamp + : time(); } } diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 98536ce41eec..d880f8df63d3 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -237,7 +237,9 @@ protected function addToParent(SymfonyCommand $command) public function resolve($command) { if (is_subclass_of($command, SymfonyCommand::class) && ($commandName = $command::getDefaultName())) { - $this->commandMap[$commandName] = $command; + foreach (explode('|', $commandName) as $name) { + $this->commandMap[$name] = $command; + } return null; } diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index 7e1b3a1ff6ed..1c6d949fd12f 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -236,11 +236,13 @@ protected function commandIsolationMutex() */ protected function resolveCommand($command) { - if (! class_exists($command)) { - return $this->getApplication()->find($command); - } + if (is_string($command)) { + if (! class_exists($command)) { + return $this->getApplication()->find($command); + } - $command = $this->laravel->make($command); + $command = $this->laravel->make($command); + } if ($command instanceof SymfonyCommand) { $command->setApplication($this->getApplication()); diff --git a/src/Illuminate/Console/Concerns/InteractsWithSignals.php b/src/Illuminate/Console/Concerns/InteractsWithSignals.php index 895072c15c72..c93b98dc4e6d 100644 --- a/src/Illuminate/Console/Concerns/InteractsWithSignals.php +++ b/src/Illuminate/Console/Concerns/InteractsWithSignals.php @@ -17,7 +17,9 @@ trait InteractsWithSignals /** * Define a callback to be run when the given signal(s) occurs. * - * @param iterable|int $signals + * @template TSignals of iterable|int + * + * @param (\Closure():(TSignals))|TSignals $signals * @param callable(int $signal): void $callback * @return void */ @@ -28,7 +30,7 @@ public function trap($signals, $callback) $this->getApplication()->getSignalRegistry(), ); - collect(Arr::wrap($signals)) + collect(Arr::wrap(value($signals))) ->each(fn ($signal) => $this->signals->register($signal, $callback)); }); } diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index f061dc67d384..1dc5ed792ee2 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -84,6 +84,7 @@ abstract class GeneratorCommand extends Command implements PromptsForMissingInpu 'namespace', 'new', 'or', + 'parent', 'print', 'private', 'protected', @@ -249,7 +250,7 @@ protected function possibleModels() { $modelPath = is_dir(app_path('Models')) ? app_path('Models') : app_path(); - return collect((new Finder)->files()->depth(0)->in($modelPath)) + return collect(Finder::create()->files()->depth(0)->in($modelPath)) ->map(fn ($file) => $file->getBasename('.php')) ->sort() ->values() @@ -269,7 +270,7 @@ protected function possibleEvents() return []; } - return collect((new Finder)->files()->depth(0)->in($eventPath)) + return collect(Finder::create()->files()->depth(0)->in($eventPath)) ->map(fn ($file) => $file->getBasename('.php')) ->sort() ->values() diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index f6386b89d48b..917ff9a183b1 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -271,7 +271,7 @@ protected function shouldInterrupt() /** * Ensure the interrupt signal is cleared. * - * @return bool + * @return void */ protected function clearInterruptSignal() { diff --git a/src/Illuminate/Contracts/Translation/Translator.php b/src/Illuminate/Contracts/Translation/Translator.php index 6eae4915d5a1..ded1a8b864f9 100644 --- a/src/Illuminate/Contracts/Translation/Translator.php +++ b/src/Illuminate/Contracts/Translation/Translator.php @@ -18,7 +18,7 @@ public function get($key, array $replace = [], $locale = null); * Get a translation according to an integer value. * * @param string $key - * @param \Countable|int|array $number + * @param \Countable|int|float|array $number * @param array $replace * @param string|null $locale * @return string diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index 0d45b0a6f6fc..c41a58b32687 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -379,11 +379,14 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = $orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems()); if (! is_null($cursor)) { - $addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) { - $unionBuilders = isset($builder->unions) ? collect($builder->unions)->pluck('query') : collect(); + // Reset the union bindings so we can add the cursor where in the correct position... + $this->setBindings([], 'union'); + + $addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) { + $unionBuilders = $builder->getUnionBuilders(); if (! is_null($previousColumn)) { - $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $previousColumn); + $originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn); $builder->where( Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn, @@ -393,7 +396,7 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = $unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) { $unionBuilder->where( - $this->getOriginalColumnNameForCursorPagination($this, $previousColumn), + $this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn), '=', $cursor->parameter($previousColumn) ); @@ -402,44 +405,48 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName = }); } - $builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) { + $builder->where(function (self $secondBuilder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) { ['column' => $column, 'direction' => $direction] = $orders[$i]; $originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $column); - $builder->where( + $secondBuilder->where( Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn, $direction === 'asc' ? '>' : '<', $cursor->parameter($column) ); if ($i < $orders->count() - 1) { - $builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) { - $addCursorConditions($builder, $column, $i + 1); + $secondBuilder->orWhere(function (self $thirdBuilder) use ($addCursorConditions, $column, $originalColumn, $i) { + $addCursorConditions($thirdBuilder, $column, $originalColumn, $i + 1); }); } $unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) { - $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) { + $unionWheres = $unionBuilder->getRawBindings()['where']; + + $originalColumn = $this->getOriginalColumnNameForCursorPagination($unionBuilder, $column); + $unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions, $originalColumn, $unionWheres) { $unionBuilder->where( - $this->getOriginalColumnNameForCursorPagination($this, $column), + $originalColumn, $direction === 'asc' ? '>' : '<', $cursor->parameter($column) ); if ($i < $orders->count() - 1) { - $unionBuilder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) { - $addCursorConditions($builder, $column, $i + 1); + $unionBuilder->orWhere(function (self $fourthBuilder) use ($addCursorConditions, $column, $originalColumn, $i) { + $addCursorConditions($fourthBuilder, $column, $originalColumn, $i + 1); }); } + $this->addBinding($unionWheres, 'union'); $this->addBinding($unionBuilder->getRawBindings()['where'], 'union'); }); }); }); }; - $addCursorConditions($this, null, 0); + $addCursorConditions($this, null, null, 0); } $this->limit($perPage + 1); diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 99670cf0949c..ce0342ec5010 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -41,22 +41,15 @@ public function transaction(Closure $callback, $attempts = 1) continue; } + $levelBeingCommitted = $this->transactions; + try { if ($this->transactions == 1) { $this->fireConnectionEvent('committing'); $this->getPdo()->commit(); } - [$levelBeingCommitted, $this->transactions] = [ - $this->transactions, - max(0, $this->transactions - 1), - ]; - - $this->transactionsManager?->commit( - $this->getName(), - $levelBeingCommitted, - $this->transactions - ); + $this->transactions = max(0, $this->transactions - 1); } catch (Throwable $e) { $this->handleCommitTransactionException( $e, $currentAttempt, $attempts @@ -65,6 +58,12 @@ public function transaction(Closure $callback, $attempts = 1) continue; } + $this->transactionsManager?->commit( + $this->getName(), + $levelBeingCommitted, + $this->transactions + ); + $this->fireConnectionEvent('committed'); return $callbackResult; @@ -119,6 +118,10 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma */ public function beginTransaction() { + foreach ($this->beforeStartingTransaction as $callback) { + $callback($this); + } + $this->createTransaction(); $this->transactions++; diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index a46448bb8974..f55adabc57cc 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -182,6 +182,13 @@ class Connection implements ConnectionInterface */ protected $pretending = false; + /** + * All of the callbacks that should be invoked before a transaction is started. + * + * @var \Closure[] + */ + protected $beforeStartingTransaction = []; + /** * All of the callbacks that should be invoked before a query is executed. * @@ -1021,6 +1028,19 @@ public function disconnect() $this->doctrineConnection = null; } + /** + * Register a hook to be run just before a database transaction is started. + * + * @param \Closure $callback + * @return $this + */ + public function beforeStartingTransaction(Closure $callback) + { + $this->beforeStartingTransaction[] = $callback; + + return $this; + } + /** * Register a hook to be run just before a database query is executed. * diff --git a/src/Illuminate/Database/Connectors/PostgresConnector.php b/src/Illuminate/Database/Connectors/PostgresConnector.php index 5ec0e70437d1..9834d2ce639f 100755 --- a/src/Illuminate/Database/Connectors/PostgresConnector.php +++ b/src/Illuminate/Database/Connectors/PostgresConnector.php @@ -38,8 +38,6 @@ public function connect(array $config) $this->configureIsolationLevel($connection, $config); - $this->configureEncoding($connection, $config); - // Next, we will check to see if a timezone has been specified in this config // and if it has we will issue a statement to modify the timezone with the // database. Setting this DB timezone is an optional configuration item. @@ -47,11 +45,6 @@ public function connect(array $config) $this->configureSearchPath($connection, $config); - // Postgres allows an application_name to be set by the user and this name is - // used to when monitoring the application with pg_stat_activity. So we'll - // determine if the option has been specified and run a statement if so. - $this->configureApplicationName($connection, $config); - $this->configureSynchronousCommit($connection, $config); return $connection; @@ -71,22 +64,6 @@ protected function configureIsolationLevel($connection, array $config) } } - /** - * Set the connection character set and collation. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureEncoding($connection, $config) - { - if (! isset($config['charset'])) { - return; - } - - $connection->prepare("set names '{$config['charset']}'")->execute(); - } - /** * Set the timezone on the connection. * @@ -132,22 +109,6 @@ protected function quoteSearchPath($searchPath) return count($searchPath) === 1 ? '"'.$searchPath[0].'"' : '"'.implode('", "', $searchPath).'"'; } - /** - * Set the application name on the connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureApplicationName($connection, $config) - { - if (isset($config['application_name'])) { - $applicationName = $config['application_name']; - - $connection->prepare("set application_name to '$applicationName'")->execute(); - } - } - /** * Create a DSN string from a configuration. * @@ -178,6 +139,17 @@ protected function getDsn(array $config) $dsn .= ";port={$port}"; } + if (isset($charset)) { + $dsn .= ";client_encoding='{$charset}'"; + } + + // Postgres allows an application_name to be set by the user and this name is + // used to when monitoring the application with pg_stat_activity. So we'll + // determine if the option has been specified and run a statement if so. + if (isset($application_name)) { + $dsn .= ";application_name='".str_replace("'", "\'", $application_name)."'"; + } + return $this->addSslOptions($dsn, $config); } diff --git a/src/Illuminate/Database/Console/DbCommand.php b/src/Illuminate/Database/Console/DbCommand.php index caecafe3a644..c9d3b5290907 100644 --- a/src/Illuminate/Database/Console/DbCommand.php +++ b/src/Illuminate/Database/Console/DbCommand.php @@ -192,6 +192,7 @@ protected function getSqlsrvArguments(array $connection) 'password' => ['-P', $connection['password']], 'host' => ['-S', 'tcp:'.$connection['host'] .($connection['port'] ? ','.$connection['port'] : ''), ], + 'trust_server_certificate' => ['-C'], ], $connection)); } diff --git a/src/Illuminate/Database/Console/PruneCommand.php b/src/Illuminate/Database/Console/PruneCommand.php index 2cb0a603ef79..23875d1187b9 100644 --- a/src/Illuminate/Database/Console/PruneCommand.php +++ b/src/Illuminate/Database/Console/PruneCommand.php @@ -126,7 +126,7 @@ protected function models() throw new InvalidArgumentException('The --models and --except options cannot be combined.'); } - return collect((new Finder)->in($this->getPath())->files()->name('*.php')) + return collect(Finder::create()->in($this->getPath())->files()->name('*.php')) ->map(function ($model) { $namespace = $this->laravel->getNamespace(); diff --git a/src/Illuminate/Database/DBAL/TimestampType.php b/src/Illuminate/Database/DBAL/TimestampType.php index e702523925a1..b5d9777503d9 100644 --- a/src/Illuminate/Database/DBAL/TimestampType.php +++ b/src/Illuminate/Database/DBAL/TimestampType.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MariaDb1052Platform; +use Doctrine\DBAL\Platforms\MariaDb1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; @@ -34,7 +35,8 @@ public function getSQLDeclaration(array $column, AbstractPlatform $platform): st MySQL80Platform::class, MariaDBPlatform::class, MariaDb1027Platform::class, - MariaDb1052Platform::class, => $this->getMySqlPlatformSQLDeclaration($column), + MariaDb1052Platform::class, + MariaDb1060Platform::class => $this->getMySqlPlatformSQLDeclaration($column), PostgreSQLPlatform::class, PostgreSQL94Platform::class, PostgreSQL100Platform::class => $this->getPostgresPlatformSQLDeclaration($column), diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index c730dc503ac2..ee2889a2d18a 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -83,7 +83,8 @@ public function commit($connection, $levelBeingCommitted, $newTransactionLevel) // shouldn't be any pending transactions, but going to clear them here anyways just // in case. This method could be refactored to receive a level in the future too. $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection === $connection + fn ($transaction) => $transaction->connection === $connection && + $transaction->level >= $levelBeingCommitted )->values(); [$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition( diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index 32492eb0729c..8cb1187a8bb7 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -19,6 +19,7 @@ protected function causedByLostConnection(Throwable $e) return Str::contains($message, [ 'server has gone away', + 'Server has gone away', 'no connection to the server', 'Lost connection', 'is dead or not enabled', @@ -71,6 +72,7 @@ protected function causedByLostConnection(Throwable $e) 'SQLSTATE[HY000] [2002] The requested address is not valid in its context', 'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network', 'SQLSTATE[HY000]: General error: 3989', + 'went away', ]); } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/ObservedBy.php b/src/Illuminate/Database/Eloquent/Attributes/ObservedBy.php new file mode 100644 index 000000000000..600174146f94 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/ObservedBy.php @@ -0,0 +1,19 @@ +contains('or')) { + if ($whereBooleans->contains(fn ($logicalOperator) => str_contains($logicalOperator, 'or'))) { $query->wheres[] = $this->createNestedWhere( - $whereSlice, $whereBooleans->first() + $whereSlice, str_replace(' not', '', $whereBooleans->first()) ); } else { $query->wheres = array_merge($query->wheres, $whereSlice); @@ -1726,6 +1728,18 @@ public function withSavepointIfNeeded(Closure $scope): mixed : $scope(); } + /** + * Get the Eloquent builder instances that are used in the union of the query. + * + * @return \Illuminate\Support\Collection + */ + protected function getUnionBuilders() + { + return isset($this->query->unions) + ? collect($this->query->unions)->pluck('query') + : collect(); + } + /** * Get the underlying query builder instance. * diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php index f5f0571bca50..7909b197b87b 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php @@ -30,7 +30,7 @@ public function __construct(array $arguments) public function get($model, $key, $value, $attributes) { - if (! isset($attributes[$key]) || is_null($attributes[$key])) { + if (! isset($attributes[$key])) { return; } diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php index ff632ed223cd..926881287ef7 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php @@ -30,7 +30,7 @@ public function __construct(array $arguments) public function get($model, $key, $value, $attributes) { - if (! isset($attributes[$key]) || is_null($attributes[$key])) { + if (! isset($attributes[$key])) { return; } diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index b7e0d7dea8c0..f7d4c9ff538d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -7,7 +7,7 @@ trait GuardsAttributes /** * The attributes that are mass assignable. * - * @var array + * @var array */ protected $fillable = []; diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 029d45637c63..c2c478d1cb59 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1271,7 +1271,11 @@ protected function asJson($value) */ public function fromJson($value, $asObject = false) { - return Json::decode($value ?? '', ! $asObject); + if ($value === null || $value === '') { + return null; + } + + return Json::decode($value, ! $asObject); } /** @@ -2101,7 +2105,7 @@ public function originalIsEquivalent($key) } return is_numeric($attribute) && is_numeric($original) - && BigDecimal::of($attribute)->isEqualTo($original); + && strcmp((string) $attribute, (string) $original) === 0; } /** diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php index 37bc063aaa85..0730dcb10971 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php @@ -3,9 +3,11 @@ namespace Illuminate\Database\Eloquent\Concerns; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Events\NullDispatcher; use Illuminate\Support\Arr; use InvalidArgumentException; +use ReflectionClass; trait HasEvents { @@ -27,6 +29,31 @@ trait HasEvents */ protected $observables = []; + /** + * Boot the has event trait for a model. + * + * @return void + */ + public static function bootHasEvents() + { + static::observe(static::resolveObserveAttributes()); + } + + /** + * Resolve the observe class names from the attributes. + * + * @return array + */ + public static function resolveObserveAttributes() + { + $reflectionClass = new ReflectionClass(static::class); + + return collect($reflectionClass->getAttributes(ObservedBy::class)) + ->map(fn ($attribute) => $attribute->getArguments()) + ->flatten() + ->all(); + } + /** * Register observers with the model. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php index 5d7047953115..0913d94b372a 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -3,12 +3,39 @@ namespace Illuminate\Database\Eloquent\Concerns; use Closure; +use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Arr; use InvalidArgumentException; +use ReflectionClass; trait HasGlobalScopes { + /** + * Boot the has global scopes trait for a model. + * + * @return void + */ + public static function bootHasGlobalScopes() + { + static::addGlobalScopes(static::resolveGlobalScopeAttributes()); + } + + /** + * Resolve the global scope class names from the attributes. + * + * @return array + */ + public static function resolveGlobalScopeAttributes() + { + $reflectionClass = new ReflectionClass(static::class); + + return collect($reflectionClass->getAttributes(ScopedBy::class)) + ->map(fn ($attribute) => $attribute->getArguments()) + ->flatten() + ->all(); + } + /** * Register a new global scope on the model. * @@ -26,9 +53,28 @@ public static function addGlobalScope($scope, $implementation = null) return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope; } elseif ($scope instanceof Scope) { return static::$globalScopes[static::class][get_class($scope)] = $scope; + } elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) { + return static::$globalScopes[static::class][$scope] = new $scope; } - throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.'); + throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope or be a class name of a class extending '.Scope::class); + } + + /** + * Register multiple global scopes on the model. + * + * @param array $scopes + * @return void + */ + public static function addGlobalScopes(array $scopes) + { + foreach ($scopes as $key => $scope) { + if (is_string($key)) { + static::addGlobalScope($key, $scope); + } else { + static::addGlobalScope($scope); + } + } } /** diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 154717fa4d81..de43f9839824 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -599,7 +599,7 @@ public function orWhereBelongsTo($related, $relationshipName = null) * Add subselect queries to include an aggregate value for a relationship. * * @param mixed $relations - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @param string $function * @return $this */ @@ -630,15 +630,19 @@ public function withAggregate($relations, $column, $function = null) $relation = $this->getRelationWithoutConstraints($name); if ($function) { - $hashedColumn = $this->getRelationHashedColumn($column, $relation); + if ($this->getQuery()->getGrammar()->isExpression($column)) { + $aggregateColumn = $this->getQuery()->getGrammar()->getValue($column); + } else { + $hashedColumn = $this->getRelationHashedColumn($column, $relation); - $wrappedColumn = $this->getQuery()->getGrammar()->wrap( - $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) - ); + $aggregateColumn = $this->getQuery()->getGrammar()->wrap( + $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) + ); + } - $expression = $function === 'exists' ? $wrappedColumn : sprintf('%s(%s)', $function, $wrappedColumn); + $expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn); } else { - $expression = $column; + $expression = $this->getQuery()->getGrammar()->getValue($column); } // Here, we will grab the relationship sub-query and prepare to add it to the main query @@ -667,7 +671,7 @@ public function withAggregate($relations, $column, $function = null) // the query builder. Then, we will return the builder instance back to the developer // for further constraint chaining that needs to take place on the query as needed. $alias ??= Str::snake( - preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column") + preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function {$this->getQuery()->getGrammar()->getValue($column)}") ); if ($function === 'exists') { @@ -719,7 +723,7 @@ public function withCount($relations) * Add subselect queries to include the max of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withMax($relation, $column) @@ -731,7 +735,7 @@ public function withMax($relation, $column) * Add subselect queries to include the min of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withMin($relation, $column) @@ -743,7 +747,7 @@ public function withMin($relation, $column) * Add subselect queries to include the sum of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withSum($relation, $column) @@ -755,7 +759,7 @@ public function withSum($relation, $column) * Add subselect queries to include the average of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withAvg($relation, $column) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index d9a453359d23..63df5b8363ec 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -676,7 +676,7 @@ public function afterMaking(Closure $callback) /** * Add a new "after creating" callback to the model definition. * - * @param \Closure(\Illuminate\Database\Eloquent\Model|TModel): mixed $callback + * @param \Closure(\Illuminate\Database\Eloquent\Model|TModel, \Illuminate\Database\Eloquent\Model|null): mixed $callback * @return static */ public function afterCreating(Closure $callback) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index ee4c10aae4c4..48618cbbd61f 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -20,6 +20,7 @@ use Illuminate\Support\Collection as BaseCollection; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; +use JsonException; use JsonSerializable; use LogicException; @@ -1646,10 +1647,10 @@ public function toArray() */ public function toJson($options = 0) { - $json = json_encode($this->jsonSerialize(), $options); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw JsonEncodingException::forModel($this, json_last_error_msg()); + try { + $json = json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw JsonEncodingException::forModel($this, $e->getMessage()); } return $json; @@ -2122,7 +2123,7 @@ protected function childRouteBindingRelationshipName($childType) /** * Retrieve the model for a bound value. * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation $query + * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Contracts\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query * @param mixed $value * @param string|null $field * @return \Illuminate\Database\Eloquent\Relations\Relation diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php index 8bde76a0c388..112a0edba022 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent\Relations; +use BackedEnum; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; @@ -375,7 +376,9 @@ protected function getRelatedKeyFrom(Model $model) */ protected function getForeignKeyFrom(Model $model) { - return $model->{$this->foreignKey}; + $foreignKey = $model->{$this->foreignKey}; + + return $foreignKey instanceof BackedEnum ? $foreignKey->value : $foreignKey; } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 37c698f3d80f..0f93ee7e390c 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -997,19 +997,66 @@ public function chunk($count, callable $callback) */ public function chunkById($count, callable $callback, $column = null, $alias = null) { - $this->prepareQueryBuilder(); + return $this->orderedChunkById($count, $callback, $column, $alias); + } + + /** + * Chunk the results of a query by comparing IDs in descending order. + * + * @param int $count + * @param callable $callback + * @param string|null $column + * @param string|null $alias + * @return bool + */ + public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null) + { + return $this->orderedChunkById($count, $callback, $column, $alias, descending: true); + } + /** + * Execute a callback over each item while chunking by ID. + * + * @param callable $callback + * @param int $count + * @param string|null $column + * @param string|null $alias + * @return bool + */ + public function eachById(callable $callback, $count = 1000, $column = null, $alias = null) + { + return $this->chunkById($count, function ($results, $page) use ($callback, $count) { + foreach ($results as $key => $value) { + if ($callback($value, (($page - 1) * $count) + $key) === false) { + return false; + } + } + }, $column, $alias); + } + + /** + * Chunk the results of a query by comparing IDs in a given order. + * + * @param int $count + * @param callable $callback + * @param string|null $column + * @param string|null $alias + * @param bool $descending + * @return bool + */ + public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false) + { $column ??= $this->getRelated()->qualifyColumn( $this->getRelatedKeyName() ); $alias ??= $this->getRelatedKeyName(); - return $this->query->chunkById($count, function ($results) use ($callback) { + return $this->prepareQueryBuilder()->orderedChunkById($count, function ($results, $page) use ($callback) { $this->hydratePivotRelation($results->all()); - return $callback($results); - }, $column, $alias); + return $callback($results, $page); + }, $column, $alias, $descending); } /** @@ -1068,6 +1115,29 @@ public function lazyById($chunkSize = 1000, $column = null, $alias = null) }); } + /** + * Query lazily, by chunking the results of a query by comparing IDs in descending order. + * + * @param int $chunkSize + * @param string|null $column + * @param string|null $alias + * @return \Illuminate\Support\LazyCollection + */ + public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) + { + $column ??= $this->getRelated()->qualifyColumn( + $this->getRelatedKeyName() + ); + + $alias ??= $this->getRelatedKeyName(); + + return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias)->map(function ($model) { + $this->hydratePivotRelation([$model]); + + return $model; + }); + } + /** * Get a lazy collection for the given query. * @@ -1179,6 +1249,10 @@ protected function guessInverseRelation() */ public function touch() { + if ($this->related->isIgnoringTouch()) { + return; + } + $columns = [ $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(), ]; diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 04862e5d80f7..48444a52db85 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; +use BackedEnum; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; @@ -154,6 +155,10 @@ protected function formatRecordsList(array $records) [$id, $attributes] = [$attributes, []]; } + if ($id instanceof BackedEnum) { + $id = $id->value; + } + return [$id => $attributes]; })->all(); } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index b0b4b1fdebe1..20c34749efba 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -600,6 +600,24 @@ public function chunkById($count, callable $callback, $column = null, $alias = n return $this->prepareQueryBuilder()->chunkById($count, $callback, $column, $alias); } + /** + * Chunk the results of a query by comparing IDs in descending order. + * + * @param int $count + * @param callable $callback + * @param string|null $column + * @param string|null $alias + * @return bool + */ + public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null) + { + $column ??= $this->getRelated()->getQualifiedKeyName(); + + $alias ??= $this->getRelated()->getKeyName(); + + return $this->prepareQueryBuilder()->chunkByIdDesc($count, $callback, $column, $alias); + } + /** * Execute a callback over each item while chunking by ID. * @@ -674,6 +692,23 @@ public function lazyById($chunkSize = 1000, $column = null, $alias = null) return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias); } + /** + * Query lazily, by chunking the results of a query by comparing IDs in descending order. + * + * @param int $chunkSize + * @param string|null $column + * @param string|null $alias + * @return \Illuminate\Support\LazyCollection + */ + public function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) + { + $column ??= $this->getRelated()->getQualifiedKeyName(); + + $alias ??= $this->getRelated()->getKeyName(); + + return $this->prepareQueryBuilder()->lazyByIdDesc($chunkSize, $column, $alias); + } + /** * Prepare the query builder for query execution. * diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php index 5ca8b48bed02..39c7852f2888 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php @@ -68,6 +68,8 @@ public function delete() $query->where($this->morphType, $this->morphClass); return tap($query->delete(), function () { + $this->exists = false; + $this->fireModelEvent('deleted', false); }); } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 87b8e7816f9f..8cf113bd0f34 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -189,6 +189,16 @@ public function getMorphType() return $this->morphType; } + /** + * Get the fully qualified morph type for the relation. + * + * @return string + */ + public function getQualifiedMorphTypeName() + { + return $this->qualifyPivotColumn($this->morphType); + } + /** * Get the class name of the parent model. * diff --git a/src/Illuminate/Database/Eloquent/Relations/Pivot.php b/src/Illuminate/Database/Eloquent/Relations/Pivot.php index a65ecdea6633..6e1d3f27897e 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Pivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/Pivot.php @@ -19,7 +19,7 @@ class Pivot extends Model /** * The attributes that aren't mass assignable. * - * @var array + * @var array|bool */ protected $guarded = []; } diff --git a/src/Illuminate/Database/MySqlConnection.php b/src/Illuminate/Database/MySqlConnection.php index 460a4fd375c1..9db946ab11e0 100755 --- a/src/Illuminate/Database/MySqlConnection.php +++ b/src/Illuminate/Database/MySqlConnection.php @@ -14,6 +14,42 @@ class MySqlConnection extends Connection { + /** + * The last inserted ID generated by the server. + * + * @var string|int|null + */ + protected $lastInsertId; + + /** + * Run an insert statement against the database. + * + * @param string $query + * @param array $bindings + * @param string|null $sequence + * @return bool + */ + public function insert($query, $bindings = [], $sequence = null) + { + return $this->run($query, $bindings, function ($query, $bindings) use ($sequence) { + if ($this->pretending()) { + return true; + } + + $statement = $this->getPdo()->prepare($query); + + $this->bindValues($statement, $this->prepareBindings($bindings)); + + $this->recordsHaveBeenModified(); + + $result = $statement->execute(); + + $this->lastInsertId = $this->getPdo()->lastInsertId($sequence); + + return $result; + }); + } + /** * Escape a binary value for safe SQL embedding. * @@ -38,6 +74,16 @@ protected function isUniqueConstraintError(Exception $exception) return boolval(preg_match('#Integrity constraint violation: 1062#i', $exception->getMessage())); } + /** + * Get the connection's last insert ID. + * + * @return string|int|null + */ + public function getLastInsertId() + { + return $this->lastInsertId; + } + /** * Determine if the connected database is a MariaDB database. * diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 62abc80023c8..9f87562f01d6 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -98,7 +98,7 @@ class Builder implements BuilderContract /** * The table which the query is targeting. * - * @var string + * @var \Illuminate\Database\Query\Expression|string */ public $from; @@ -510,7 +510,7 @@ public function ignoreIndex($index) * Add a join clause to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @param string $type @@ -550,7 +550,7 @@ public function join($table, $first, $operator = null, $second = null, $type = ' * Add a "join where" clause to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string $operator * @param \Illuminate\Contracts\Database\Query\Expression|string $second * @param string $type @@ -566,7 +566,7 @@ public function joinWhere($table, $first, $operator, $second, $type = 'inner') * * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query * @param string $as - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @param string $type @@ -586,11 +586,44 @@ public function joinSub($query, $as, $first, $operator = null, $second = null, $ return $this->join(new Expression($expression), $first, $operator, $second, $type, $where); } + /** + * Add a lateral join clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @param string $type + * @return $this + */ + public function joinLateral($query, string $as, string $type = 'inner') + { + [$query, $bindings] = $this->createSub($query); + + $expression = '('.$query.') as '.$this->grammar->wrapTable($as); + + $this->addBinding($bindings, 'join'); + + $this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression)); + + return $this; + } + + /** + * Add a lateral left join to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @return $this + */ + public function leftJoinLateral($query, string $as) + { + return $this->joinLateral($query, $as, 'left'); + } + /** * Add a left join to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this @@ -604,7 +637,7 @@ public function leftJoin($table, $first, $operator = null, $second = null) * Add a "join where" clause to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this @@ -619,7 +652,7 @@ public function leftJoinWhere($table, $first, $operator, $second) * * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query * @param string $as - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this @@ -647,7 +680,7 @@ public function rightJoin($table, $first, $operator = null, $second = null) * Add a "right join where" clause to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string $operator * @param \Illuminate\Contracts\Database\Query\Expression|string $second * @return $this @@ -662,7 +695,7 @@ public function rightJoinWhere($table, $first, $operator, $second) * * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query * @param string $as - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this @@ -676,7 +709,7 @@ public function rightJoinSub($query, $as, $first, $operator = null, $second = nu * Add a "cross join" clause to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table - * @param \Closure|string|null $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string|null $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this @@ -725,6 +758,19 @@ protected function newJoinClause(self $parentQuery, $type, $table) return new JoinClause($parentQuery, $type, $table); } + /** + * Get a new join lateral clause. + * + * @param \Illuminate\Database\Query\Builder $parentQuery + * @param string $type + * @param string $table + * @return \Illuminate\Database\Query\JoinLateralClause + */ + protected function newJoinLateralClause(self $parentQuery, $type, $table) + { + return new JoinLateralClause($parentQuery, $type, $table); + } + /** * Merge an array of where clauses and bindings. * @@ -983,7 +1029,7 @@ public function orWhereNot($column, $operator = null, $value = null) /** * Add a "where" clause comparing two columns to the query. * - * @param string|array $first + * @param \Illuminate\Contracts\Database\Query\Expression|string|array $first * @param string|null $operator * @param string|null $second * @param string|null $boolean @@ -1020,7 +1066,7 @@ public function whereColumn($first, $operator = null, $second = null, $boolean = /** * Add an "or where" clause comparing two columns to the query. * - * @param string|array $first + * @param \Illuminate\Contracts\Database\Query\Expression|string|array $first * @param string|null $operator * @param string|null $second * @return $this @@ -1161,7 +1207,7 @@ public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = fal $values = Arr::flatten($values); foreach ($values as &$value) { - $value = (int) $value; + $value = (int) ($value instanceof BackedEnum ? $value->value : $value); } $this->wheres[] = compact('type', 'column', 'values', 'boolean'); @@ -1658,7 +1704,7 @@ public function addNestedWhereQuery($query, $boolean = 'and') * * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @param string $operator - * @param \Closure||\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $callback + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $callback * @param string $boolean * @return $this */ @@ -2049,6 +2095,80 @@ public function orWhereFullText($columns, $value, array $options = []) return $this->whereFulltext($columns, $value, $options, 'or'); } + /** + * Add a "where" clause to the query for multiple columns with "and" conditions between them. + * + * @param string[] $columns + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * @return $this + */ + public function whereAll($columns, $operator = null, $value = null, $boolean = 'and') + { + [$value, $operator] = $this->prepareValueAndOperator( + $value, $operator, func_num_args() === 2 + ); + + $this->whereNested(function ($query) use ($columns, $operator, $value) { + foreach ($columns as $column) { + $query->where($column, $operator, $value, 'and'); + } + }, $boolean); + + return $this; + } + + /** + * Add an "or where" clause to the query for multiple columns with "and" conditions between them. + * + * @param string[] $columns + * @param string $operator + * @param mixed $value + * @return $this + */ + public function orWhereAll($columns, $operator = null, $value = null) + { + return $this->whereAll($columns, $operator, $value, 'or'); + } + + /** + * Add an "where" clause to the query for multiple columns with "or" conditions between them. + * + * @param string[] $columns + * @param string $operator + * @param mixed $value + * @param string $boolean + * @return $this + */ + public function whereAny($columns, $operator = null, $value = null, $boolean = 'and') + { + [$value, $operator] = $this->prepareValueAndOperator( + $value, $operator, func_num_args() === 2 + ); + + $this->whereNested(function ($query) use ($columns, $operator, $value) { + foreach ($columns as $column) { + $query->where($column, $operator, $value, 'or'); + } + }, $boolean); + + return $this; + } + + /** + * Add an "or where" clause to the query for multiple columns with "or" conditions between them. + * + * @param string[] $columns + * @param string $operator + * @param mixed $value + * @return $this + */ + public function orWhereAny($columns, $operator = null, $value = null) + { + return $this->whereAny($columns, $operator, $value, 'or'); + } + /** * Add a "group by" clause to the query. * @@ -2896,10 +3016,10 @@ protected function runPaginationCountQuery($columns = ['*']) ->get()->all(); } - $without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset']; + $without = $this->unions ? ['unionOrders', 'unionLimit', 'unionOffset'] : ['columns', 'orders', 'limit', 'offset']; return $this->cloneWithout($without) - ->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order']) + ->cloneWithoutBindings($this->unions ? ['unionOrder'] : ['select', 'order']) ->setAggregate('count', $this->withoutSelectAliases($columns)) ->get()->all(); } @@ -3406,6 +3526,25 @@ public function insertUsing(array $columns, $query) ); } + /** + * Insert new records into the table using a subquery while ignoring errors. + * + * @param array $columns + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @return int + */ + public function insertOrIgnoreUsing(array $columns, $query) + { + $this->applyBeforeQueryCallbacks(); + + [$sql, $bindings] = $this->createSub($query); + + return $this->connection->affectingStatement( + $this->grammar->compileInsertOrIgnoreUsing($this, $columns, $sql), + $this->cleanBindings($bindings) + ); + } + /** * Update records in the database. * @@ -3676,6 +3815,18 @@ public function raw($value) return $this->connection->raw($value); } + /** + * Get the query builder instances that are used in the union of the query. + * + * @return \Illuminate\Support\Collection + */ + protected function getUnionBuilders() + { + return isset($this->unions) + ? collect($this->unions)->pluck('query') + : collect(); + } + /** * Get the current query value bindings in a flattened array. * diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 3b4f117693f6..b8eed21e69fd 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -7,6 +7,7 @@ use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use RuntimeException; @@ -182,10 +183,28 @@ protected function compileJoins(Builder $query, $joins) $tableAndNestedJoins = is_null($join->joins) ? $table : '('.$table.$nestedJoins.')'; + if ($join instanceof JoinLateralClause) { + return $this->compileJoinLateral($join, $tableAndNestedJoins); + } + return trim("{$join->type} join {$tableAndNestedJoins} {$this->compileWheres($join)}"); })->implode(' '); } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + * + * @throws \RuntimeException + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + throw new RuntimeException('This database engine does not support lateral joins.'); + } + /** * Compile the "where" portions of the query. * @@ -1090,6 +1109,21 @@ public function compileInsertUsing(Builder $query, array $columns, string $sql) return "insert into {$table} ({$this->columnize($columns)}) $sql"; } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + * + * @throws \RuntimeException + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + throw new RuntimeException('This database engine does not support inserting while ignoring errors.'); + } + /** * Compile an update statement into SQL. * diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index b9d2a624bd9a..3d900eeb3c24 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Str; class MySqlGrammar extends Grammar @@ -106,6 +107,19 @@ public function compileInsertOrIgnore(Builder $query, array $values) return Str::replaceFirst('insert', 'insert ignore', $this->compileInsert($query, $values)); } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + return Str::replaceFirst('insert', 'insert ignore', $this->compileInsertUsing($query, $columns, $sql)); + } + /** * Compile a "JSON contains" statement into SQL. * @@ -254,6 +268,18 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar return $sql.$columns; } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + return trim("{$join->type} join lateral {$expression} on true"); + } + /** * Prepare a JSON column being updated using the JSON_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index b1786e5111cc..c22720a05c7c 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -323,6 +324,19 @@ public function compileInsertOrIgnore(Builder $query, array $values) return $this->compileInsert($query, $values).' on conflict do nothing'; } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + return $this->compileInsertUsing($query, $columns, $sql).' on conflict do nothing'; + } + /** * Compile an insert and get ID statement into SQL. * @@ -396,6 +410,18 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar return $sql.$columns; } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + return trim("{$join->type} join lateral {$expression} on true"); + } + /** * Prepares a JSON column being updated using the JSONB_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index b628d70d2b02..e4794234686d 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -212,6 +212,19 @@ public function compileInsertOrIgnore(Builder $query, array $values) return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsert($query, $values)); } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsertUsing($query, $columns, $sql)); + } + /** * Compile the columns for an update statement. * @@ -374,7 +387,7 @@ protected function compileDeleteWithJoinsOrLimit(Builder $query) public function compileTruncate(Builder $query) { return [ - 'delete from sqlite_sequence where name = ?' => [$query->from], + 'delete from sqlite_sequence where name = ?' => [$this->getTablePrefix().$query->from], 'delete from '.$this->wrapTable($query->from) => [], ]; } diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index f68722a64bce..062041c37d30 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -444,6 +445,20 @@ public function prepareBindingsForUpdate(array $bindings, array $values) ); } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + $type = $join->type == 'left' ? 'outer' : 'cross'; + + return trim("{$type} apply {$expression}"); + } + /** * Compile the SQL statement to define a savepoint. * diff --git a/src/Illuminate/Database/Query/JoinClause.php b/src/Illuminate/Database/Query/JoinClause.php index aef1c9aa547d..37a002c57245 100755 --- a/src/Illuminate/Database/Query/JoinClause.php +++ b/src/Illuminate/Database/Query/JoinClause.php @@ -82,7 +82,7 @@ public function __construct(Builder $parentQuery, $type, $table) * * on `contacts`.`user_id` = `users`.`id` and `contacts`.`info_id` = `info`.`id` * - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @param string $boolean @@ -102,7 +102,7 @@ public function on($first, $operator = null, $second = null, $boolean = 'and') /** * Add an "or on" clause to the join. * - * @param \Closure|string $first + * @param \Closure|\Illuminate\Contracts\Database\Query\Expression|string $first * @param string|null $operator * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return \Illuminate\Database\Query\JoinClause diff --git a/src/Illuminate/Database/Query/JoinLateralClause.php b/src/Illuminate/Database/Query/JoinLateralClause.php new file mode 100644 index 000000000000..1be31d29626a --- /dev/null +++ b/src/Illuminate/Database/Query/JoinLateralClause.php @@ -0,0 +1,8 @@ +getConnection()->insert($sql, $values, $sequence); + + $id = $query->getConnection()->getLastInsertId(); + + return is_numeric($id) ? (int) $id : $id; + } + /** * Process the results of a columns query. * @@ -38,7 +58,7 @@ public function processColumns($results) 'nullable' => $result->nullable === 'YES', 'default' => $result->default, 'auto_increment' => $result->extra === 'auto_increment', - 'comment' => $result->comment, + 'comment' => $result->comment ?: null, ]; }, $results); } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 770c6c52655c..4a8839442951 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -162,7 +162,7 @@ public function hasTable($table) { $table = $this->connection->getTablePrefix().$table; - foreach ($this->getTables() as $value) { + foreach ($this->getTables(false) as $value) { if (strtolower($table) === strtolower($value['name'])) { return true; } @@ -202,6 +202,16 @@ public function getTables() ); } + /** + * Get the names of the tables that belong to the database. + * + * @return array + */ + public function getTableListing() + { + return array_column($this->getTables(), 'name'); + } + /** * Get the views that belong to the database. * @@ -370,6 +380,43 @@ public function getIndexes($table) ); } + /** + * Get the names of the indexes for a given table. + * + * @param string $table + * @return array + */ + public function getIndexListing($table) + { + return array_column($this->getIndexes($table), 'name'); + } + + /** + * Determine if the given table has a given index. + * + * @param string $table + * @param string|array $index + * @param string|null $type + * @return bool + */ + public function hasIndex($table, $index, $type = null) + { + $type = is_null($type) ? $type : strtolower($type); + + foreach ($this->getIndexes($table) as $value) { + $typeMatches = is_null($type) + || ($type === 'primary' && $value['primary']) + || ($type === 'unique' && $value['unique']) + || $type === $value['type']; + + if (($value['name'] === $index || $value['columns'] === $index) && $typeMatches) { + return true; + } + } + + return false; + } + /** * Get the foreign keys for a given table. * diff --git a/src/Illuminate/Database/Schema/ColumnDefinition.php b/src/Illuminate/Database/Schema/ColumnDefinition.php index 51265ac4213e..1a7e638836b2 100644 --- a/src/Illuminate/Database/Schema/ColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ColumnDefinition.php @@ -15,7 +15,7 @@ * @method $this default(mixed $value) Specify a "default" value for the column * @method $this first() Place the column "first" in the table (MySQL) * @method $this from(int $startingValue) Set the starting value of an auto-incrementing field (MySQL / PostgreSQL) - * @method $this generatedAs(string|Expression $expression = null) Create a SQL compliant identity column (PostgreSQL) + * @method $this generatedAs(string|\Illuminate\Database\Query\Expression $expression = null) Create a SQL compliant identity column (PostgreSQL) * @method $this index(string $indexName = null) Add an index * @method $this invisible() Specify that the column should be invisible to "SELECT *" (MySQL) * @method $this nullable(bool $value = true) Allow NULL values to be inserted into the column diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index d54e9a6fe218..26d97a7a5541 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -43,11 +43,21 @@ class MySqlGrammar extends Grammar */ public function compileCreateDatabase($name, $connection) { + $charset = $connection->getConfig('charset'); + $collation = $connection->getConfig('collation'); + + if (! $charset || ! $collation) { + return sprintf( + 'create database %s', + $this->wrapValue($name), + ); + } + return sprintf( 'create database %s default character set %s default collate %s', $this->wrapValue($name), - $this->wrapValue($connection->getConfig('charset')), - $this->wrapValue($connection->getConfig('collation')), + $this->wrapValue($charset), + $this->wrapValue($collation), ); } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index acc9c5800715..6119607abd76 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -87,7 +87,7 @@ public function compileTables() { return 'select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, ' ."obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " - ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema')" + ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " .'order by c.relname'; } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 201121cb6170..d3dfdebb6392 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -123,7 +123,7 @@ public function compileColumns($table) return sprintf( 'select name, type, not "notnull" as "nullable", dflt_value as "default", pk as "primary" ' .'from pragma_table_info(%s) order by cid asc', - $this->wrap(str_replace('.', '__', $table)) + $this->quoteString(str_replace('.', '__', $table)) ); } @@ -136,12 +136,12 @@ public function compileColumns($table) public function compileIndexes($table) { return sprintf( - 'select "primary" as name, group_concat(col) as columns, 1 as "unique", 1 as "primary" ' + 'select \'primary\' as name, group_concat(col) as columns, 1 as "unique", 1 as "primary" ' .'from (select name as col from pragma_table_info(%s) where pk > 0 order by pk, cid) group by name ' - .'union select name, group_concat(col) as columns, "unique", origin = "pk" as "primary" ' + .'union select name, group_concat(col) as columns, "unique", origin = \'pk\' as "primary" ' .'from (select il.*, ii.name as col from pragma_index_list(%s) il, pragma_index_info(il.name) ii order by il.seq, ii.seqno) ' .'group by name, "unique", "primary"', - $table = $this->wrap(str_replace('.', '__', $table)), + $table = $this->quoteString(str_replace('.', '__', $table)), $table ); } @@ -159,7 +159,7 @@ public function compileForeignKeys($table) .'group_concat("to") as foreign_columns, on_update, on_delete ' .'from (select * from pragma_foreign_key_list(%s) order by id desc, seq) ' .'group by id, "table", on_update, on_delete', - $this->wrap(str_replace('.', '__', $table)) + $this->quoteString(str_replace('.', '__', $table)) ); } diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 2514c18bd6c1..5bed2f003974 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -26,7 +26,9 @@ public function dump(Connection $connection, $path) $this->removeAutoIncrementingState($path); - $this->appendMigrationData($path); + if ($this->hasMigrationTable()) { + $this->appendMigrationData($path); + } } /** diff --git a/src/Illuminate/Database/Schema/PostgresSchemaState.php b/src/Illuminate/Database/Schema/PostgresSchemaState.php index b3f9361bc92b..70ccd25b5fd3 100644 --- a/src/Illuminate/Database/Schema/PostgresSchemaState.php +++ b/src/Illuminate/Database/Schema/PostgresSchemaState.php @@ -17,9 +17,12 @@ public function dump(Connection $connection, $path) { $commands = collect([ $this->baseDumpCommand().' --schema-only > '.$path, - $this->baseDumpCommand().' -t '.$this->migrationTable.' --data-only >> '.$path, ]); + if ($this->hasMigrationTable()) { + $commands->push($this->baseDumpCommand().' -t '.$this->migrationTable.' --data-only >> '.$path); + } + $commands->map(function ($command, $path) { $this->makeProcess($command)->mustRun($this->output, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 8ae272d767b6..e7d6e8c905eb 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -34,16 +34,17 @@ public function dropDatabaseIfExists($name) /** * Get the tables for the database. * + * @param bool $withSize * @return array */ - public function getTables() + public function getTables($withSize = true) { - $withSize = false; - - try { - $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); - } catch (QueryException $e) { - // + if ($withSize) { + try { + $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); + } catch (QueryException $e) { + $withSize = false; + } } return $this->connection->getPostProcessor()->processTables( diff --git a/src/Illuminate/Database/Schema/SchemaState.php b/src/Illuminate/Database/Schema/SchemaState.php index 58d9c3a438aa..b7fa34c168c9 100644 --- a/src/Illuminate/Database/Schema/SchemaState.php +++ b/src/Illuminate/Database/Schema/SchemaState.php @@ -94,6 +94,16 @@ public function makeProcess(...$arguments) return call_user_func($this->processFactory, ...$arguments); } + /** + * Determine if the current connection has a migration table. + * + * @return bool + */ + public function hasMigrationTable(): bool + { + return $this->connection->getSchemaBuilder()->hasTable($this->migrationTable); + } + /** * Specify the name of the application's migration table. * diff --git a/src/Illuminate/Database/Schema/SqliteSchemaState.php b/src/Illuminate/Database/Schema/SqliteSchemaState.php index 10efc7c0aba9..4b66542923e4 100644 --- a/src/Illuminate/Database/Schema/SqliteSchemaState.php +++ b/src/Illuminate/Database/Schema/SqliteSchemaState.php @@ -28,7 +28,9 @@ public function dump(Connection $connection, $path) $this->files->put($path, implode(PHP_EOL, $migrations).PHP_EOL); - $this->appendMigrationData($path); + if ($this->hasMigrationTable()) { + $this->appendMigrationData($path); + } } /** diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index fcc4034eee80..d807f333456e 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -17,7 +17,7 @@ "require": { "php": "^8.1", "ext-pdo": "*", - "brick/math": "^0.9.3|^0.10.2|^0.11", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "illuminate/collections": "^10.0", "illuminate/container": "^10.0", "illuminate/contracts": "^10.0", diff --git a/src/Illuminate/Events/CallQueuedListener.php b/src/Illuminate/Events/CallQueuedListener.php index 6a39008520c8..4fb66266359a 100644 --- a/src/Illuminate/Events/CallQueuedListener.php +++ b/src/Illuminate/Events/CallQueuedListener.php @@ -68,6 +68,13 @@ class CallQueuedListener implements ShouldQueue */ public $timeout; + /** + * Indicates if the job should fail if the timeout is exceeded. + * + * @var bool + */ + public $failOnTimeout = false; + /** * Indicates if the job should be encrypted. * diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index 2f3ac4cb9a76..22993c36f94f 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -674,6 +674,7 @@ protected function propagateListenerOptions($listener, $job) $job->retryUntil = method_exists($listener, 'retryUntil') ? $listener->retryUntil(...$data) : null; $job->shouldBeEncrypted = $listener instanceof ShouldBeEncrypted; $job->timeout = $listener->timeout ?? null; + $job->failOnTimeout = $listener->failOnTimeout ?? false; $job->tries = $listener->tries ?? null; $job->through(array_merge( diff --git a/src/Illuminate/Filesystem/Filesystem.php b/src/Illuminate/Filesystem/Filesystem.php index 23fc17eeb03c..cfd4c2207835 100644 --- a/src/Illuminate/Filesystem/Filesystem.php +++ b/src/Illuminate/Filesystem/Filesystem.php @@ -16,8 +16,7 @@ class Filesystem { - use Conditionable; - use Macroable; + use Conditionable, Macroable; /** * Determine if a file or directory exists. @@ -348,7 +347,7 @@ public function copy($path, $target) * * @param string $target * @param string $link - * @return void + * @return bool|null */ public function link($target, $link) { @@ -546,7 +545,7 @@ public function hasSameHash($firstFile, $secondFile) { $hash = @md5_file($firstFile); - return $hash && $hash === @md5_file($secondFile); + return $hash && hash_equals($hash, (string) @md5_file($secondFile)); } /** diff --git a/src/Illuminate/Filesystem/LockableFile.php b/src/Illuminate/Filesystem/LockableFile.php index 8b2de765eaad..d354b884036a 100644 --- a/src/Illuminate/Filesystem/LockableFile.php +++ b/src/Illuminate/Filesystem/LockableFile.php @@ -2,7 +2,6 @@ namespace Illuminate\Filesystem; -use Exception; use Illuminate\Contracts\Filesystem\LockTimeoutException; class LockableFile @@ -67,11 +66,7 @@ protected function ensureDirectoryExists($path) */ protected function createResource($path, $mode) { - $this->handle = @fopen($path, $mode); - - if (! $this->handle) { - throw new Exception('Unable to create lockable file: '.$path.'. Please ensure you have permission to create files in this location.'); - } + $this->handle = fopen($path, $mode); } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 930c406f7ebf..b9bdd713422a 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -40,7 +40,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.40.0'; + const VERSION = '10.48.20'; /** * The base path for the Laravel installation. @@ -535,6 +535,10 @@ public function storagePath($path = '') return $this->joinPaths($this->storagePath ?: $_ENV['LARAVEL_STORAGE_PATH'], $path); } + if (isset($_SERVER['LARAVEL_STORAGE_PATH'])) { + return $this->joinPaths($this->storagePath ?: $_SERVER['LARAVEL_STORAGE_PATH'], $path); + } + return $this->joinPaths($this->storagePath ?: $this->basePath('storage'), $path); } diff --git a/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php b/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php index 3f0be6c0a605..050a96967e04 100644 --- a/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php +++ b/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php @@ -94,7 +94,7 @@ protected function createDotenv($app) * Write the error information to the screen and exit. * * @param \Dotenv\Exception\InvalidFileException $e - * @return void + * @return never */ protected function writeErrorAndDie(InvalidFileException $e) { diff --git a/src/Illuminate/Foundation/Console/Kernel.php b/src/Illuminate/Foundation/Console/Kernel.php index 227f7caea513..7b19b42fa61b 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -340,7 +340,7 @@ protected function load($paths) $namespace = $this->app->getNamespace(); - foreach ((new Finder)->in($paths)->files() as $file) { + foreach (Finder::create()->in($paths)->files() as $file) { $command = $this->commandClassFromFile($file, $namespace); if (is_subclass_of($command, Command::class) && diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index 194c80fa5127..df830148c7f7 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -57,6 +57,9 @@ class ServeCommand extends Command */ public static $passthroughVariables = [ 'APP_ENV', + 'HERD_PHP_81_INI_SCAN_DIR', + 'HERD_PHP_82_INI_SCAN_DIR', + 'HERD_PHP_83_INI_SCAN_DIR', 'IGNITION_LOCAL_SITES_PATH', 'LARAVEL_SAIL', 'PATH', @@ -140,6 +143,14 @@ protected function startProcess($hasEnvironment) return in_array($key, static::$passthroughVariables) ? [$key => $value] : [$key => false]; })->all()); + $this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) { + if ($process->isRunning()) { + $process->stop(10, $signal); + } + + exit; + }); + $process->start($this->handleProcessOutput()); return $process; @@ -284,7 +295,7 @@ protected function handleProcessOutput() $this->output->write(' '.str_repeat('.', $dots)); $this->output->writeln(" ~ {$runTime}s"); - } elseif (str($line)->contains(['Closed without sending a request'])) { + } elseif (str($line)->contains(['Closed without sending a request', 'Failed to poll event'])) { // ... } elseif (! empty($line)) { $position = strpos($line, '] '); diff --git a/src/Illuminate/Foundation/Console/StorageUnlinkCommand.php b/src/Illuminate/Foundation/Console/StorageUnlinkCommand.php new file mode 100644 index 000000000000..504bc80ab0ac --- /dev/null +++ b/src/Illuminate/Foundation/Console/StorageUnlinkCommand.php @@ -0,0 +1,53 @@ +links() as $link => $target) { + if (! file_exists($link) || ! is_link($link)) { + continue; + } + + $this->laravel->make('files')->delete($link); + + $this->components->info("The [$link] link has been deleted."); + } + } + + /** + * Get the symbolic links that are configured for the application. + * + * @return array + */ + protected function links() + { + return $this->laravel['config']['filesystems.links'] ?? + [public_path('storage') => storage_path('app/public')]; + } +} diff --git a/src/Illuminate/Foundation/Console/stubs/job.queued.stub b/src/Illuminate/Foundation/Console/stubs/job.queued.stub index bc67adcf4790..9a7cec52a433 100644 --- a/src/Illuminate/Foundation/Console/stubs/job.queued.stub +++ b/src/Illuminate/Foundation/Console/stubs/job.queued.stub @@ -3,7 +3,6 @@ namespace {{ namespace }}; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; diff --git a/src/Illuminate/Foundation/Events/DiscoverEvents.php b/src/Illuminate/Foundation/Events/DiscoverEvents.php index b285759d2188..a4728c2ef3ed 100644 --- a/src/Illuminate/Foundation/Events/DiscoverEvents.php +++ b/src/Illuminate/Foundation/Events/DiscoverEvents.php @@ -29,7 +29,7 @@ class DiscoverEvents public static function within($listenerPath, $basePath) { $listeners = collect(static::getListenerEvents( - (new Finder)->files()->in($listenerPath), $basePath + Finder::create()->files()->in($listenerPath), $basePath )); $discoveredEvents = []; diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 772a2635a3b4..4824c27c1799 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -115,11 +115,13 @@ protected function getValidatorInstance() */ protected function createDefaultValidator(ValidationFactory $factory) { - $rules = method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; + $rules = $this->validationRules(); $validator = $factory->make( - $this->validationData(), $rules, - $this->messages(), $this->attributes() + $this->validationData(), + $rules, + $this->messages(), + $this->attributes(), )->stopOnFirstFailure($this->stopOnFirstFailure); if ($this->isPrecognitive()) { @@ -141,6 +143,16 @@ public function validationData() return $this->all(); } + /** + * Get the validation rules for this form request. + * + * @return array + */ + protected function validationRules() + { + return method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; + } + /** * Handle a failed validation attempt. * diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index b92bd4ac91cb..482ccc35f620 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -70,6 +70,7 @@ use Illuminate\Foundation\Console\ScopeMakeCommand; use Illuminate\Foundation\Console\ServeCommand; use Illuminate\Foundation\Console\StorageLinkCommand; +use Illuminate\Foundation\Console\StorageUnlinkCommand; use Illuminate\Foundation\Console\StubPublishCommand; use Illuminate\Foundation\Console\TestMakeCommand; use Illuminate\Foundation\Console\UpCommand; @@ -158,6 +159,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'ScheduleInterrupt' => ScheduleInterruptCommand::class, 'ShowModel' => ShowModelCommand::class, 'StorageLink' => StorageLinkCommand::class, + 'StorageUnlink' => StorageUnlinkCommand::class, 'Up' => UpCommand::class, 'ViewCache' => ViewCacheCommand::class, 'ViewClear' => ViewClearCommand::class, diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php new file mode 100644 index 000000000000..eb699b3e967e --- /dev/null +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -0,0 +1,288 @@ +app) { + $this->refreshApplication(); + + ParallelTesting::callSetUpTestCaseCallbacks($this); + } + + $this->setUpTraits(); + + foreach ($this->afterApplicationCreatedCallbacks as $callback) { + $callback(); + } + + Model::setEventDispatcher($this->app['events']); + + $this->setUpHasRun = true; + } + + /** + * Clean up the testing environment before the next test. + * + * @internal + * + * @return void + */ + protected function tearDownTheTestEnvironment(): void + { + if ($this->app) { + $this->callBeforeApplicationDestroyedCallbacks(); + + ParallelTesting::callTearDownTestCaseCallbacks($this); + + $this->app->flush(); + + $this->app = null; + } + + $this->setUpHasRun = false; + + if (property_exists($this, 'serverVariables')) { + $this->serverVariables = []; + } + + if (property_exists($this, 'defaultHeaders')) { + $this->defaultHeaders = []; + } + + if (class_exists('Mockery')) { + if ($container = Mockery::getContainer()) { + $this->addToAssertionCount($container->mockery_getExpectationCount()); + } + + try { + Mockery::close(); + } catch (InvalidCountException $e) { + if (! Str::contains($e->getMethodName(), ['doWrite', 'askQuestion'])) { + throw $e; + } + } + } + + if (class_exists(Carbon::class)) { + Carbon::setTestNow(); + } + + if (class_exists(CarbonImmutable::class)) { + CarbonImmutable::setTestNow(); + } + + $this->afterApplicationCreatedCallbacks = []; + $this->beforeApplicationDestroyedCallbacks = []; + + if (property_exists($this, 'originalExceptionHandler')) { + $this->originalExceptionHandler = null; + } + + if (property_exists($this, 'originalDeprecationHandler')) { + $this->originalDeprecationHandler = null; + } + + AboutCommand::flushState(); + Artisan::forgetBootstrappers(); + Component::flushCache(); + Component::forgetComponentsResolver(); + Component::forgetFactory(); + ConvertEmptyStringsToNull::flushState(); + HandleExceptions::forgetApp(); + Queue::createPayloadUsing(null); + Sleep::fake(false); + TrimStrings::flushState(); + + if ($this->callbackException) { + throw $this->callbackException; + } + } + + /** + * Boot the testing helper traits. + * + * @return array + */ + protected function setUpTraits() + { + $uses = array_flip(class_uses_recursive(static::class)); + + if (isset($uses[RefreshDatabase::class])) { + $this->refreshDatabase(); + } + + if (isset($uses[DatabaseMigrations::class])) { + $this->runDatabaseMigrations(); + } + + if (isset($uses[DatabaseTruncation::class])) { + $this->truncateDatabaseTables(); + } + + if (isset($uses[DatabaseTransactions::class])) { + $this->beginDatabaseTransaction(); + } + + if (isset($uses[WithoutMiddleware::class])) { + $this->disableMiddlewareForAllTests(); + } + + if (isset($uses[WithoutEvents::class])) { + $this->disableEventsForAllTests(); + } + + if (isset($uses[WithFaker::class])) { + $this->setUpFaker(); + } + + foreach ($uses as $trait) { + if (method_exists($this, $method = 'setUp'.class_basename($trait))) { + $this->{$method}(); + } + + if (method_exists($this, $method = 'tearDown'.class_basename($trait))) { + $this->beforeApplicationDestroyed(fn () => $this->{$method}()); + } + } + + return $uses; + } + + /** + * Clean up the testing environment before the next test case. + * + * @internal + * + * @return void + */ + public static function tearDownAfterClassUsingTestCase() + { + foreach ([ + \PHPUnit\Util\Annotation\Registry::class, + \PHPUnit\Metadata\Annotation\Parser\Registry::class, + ] as $class) { + if (class_exists($class)) { + (function () { + $this->classDocBlocks = []; + $this->methodDocBlocks = []; + })->call($class::getInstance()); + } + } + } + + /** + * Register a callback to be run after the application is created. + * + * @param callable $callback + * @return void + */ + public function afterApplicationCreated(callable $callback) + { + $this->afterApplicationCreatedCallbacks[] = $callback; + + if ($this->setUpHasRun) { + $callback(); + } + } + + /** + * Register a callback to be run before the application is destroyed. + * + * @param callable $callback + * @return void + */ + protected function beforeApplicationDestroyed(callable $callback) + { + $this->beforeApplicationDestroyedCallbacks[] = $callback; + } + + /** + * Execute the application's pre-destruction callbacks. + * + * @return void + */ + protected function callBeforeApplicationDestroyedCallbacks() + { + foreach ($this->beforeApplicationDestroyedCallbacks as $callback) { + try { + $callback(); + } catch (Throwable $e) { + if (! $this->callbackException) { + $this->callbackException = $e; + } + } + } + } +} diff --git a/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php b/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php index 98204cceab48..3de593d9406b 100644 --- a/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php @@ -17,15 +17,24 @@ public function refreshDatabase() { $database = $this->app->make('db'); - $database->beforeExecuting(function () { + $callback = function () { if (RefreshDatabaseState::$lazilyRefreshed) { return; } RefreshDatabaseState::$lazilyRefreshed = true; + $shouldMockOutput = $this->mockConsoleOutput; + + $this->mockConsoleOutput = false; + $this->baseRefreshDatabase(); - }); + + $this->mockConsoleOutput = $shouldMockOutput; + }; + + $database->beforeStartingTransaction($callback); + $database->beforeExecuting($callback); $this->beforeApplicationDestroyed(function () { RefreshDatabaseState::$lazilyRefreshed = false; diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 152b5c4131ae..0b4e9367bc25 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -2,22 +2,6 @@ namespace Illuminate\Foundation\Testing; -use Carbon\CarbonImmutable; -use Illuminate\Console\Application as Artisan; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Bootstrap\HandleExceptions; -use Illuminate\Foundation\Console\AboutCommand; -use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; -use Illuminate\Foundation\Http\Middleware\TrimStrings; -use Illuminate\Queue\Queue; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Facade; -use Illuminate\Support\Facades\ParallelTesting; -use Illuminate\Support\Sleep; -use Illuminate\Support\Str; -use Illuminate\View\Component; -use Mockery; -use Mockery\Exception\InvalidCountException; use PHPUnit\Framework\TestCase as BaseTestCase; use Throwable; @@ -32,43 +16,9 @@ abstract class TestCase extends BaseTestCase Concerns\InteractsWithExceptionHandling, Concerns\InteractsWithSession, Concerns\InteractsWithTime, + Concerns\InteractsWithTestCaseLifecycle, Concerns\InteractsWithViews; - /** - * The Illuminate application instance. - * - * @var \Illuminate\Foundation\Application - */ - protected $app; - - /** - * The callbacks that should be run after the application is created. - * - * @var array - */ - protected $afterApplicationCreatedCallbacks = []; - - /** - * The callbacks that should be run before the application is destroyed. - * - * @var array - */ - protected $beforeApplicationDestroyedCallbacks = []; - - /** - * The exception thrown while running an application destruction callback. - * - * @var \Throwable - */ - protected $callbackException; - - /** - * Indicates if we have made it through the base setUp function. - * - * @var bool - */ - protected $setUpHasRun = false; - /** * Creates the application. * @@ -87,23 +37,7 @@ protected function setUp(): void { static::$latestResponse = null; - Facade::clearResolvedInstances(); - - if (! $this->app) { - $this->refreshApplication(); - - ParallelTesting::callSetUpTestCaseCallbacks($this); - } - - $this->setUpTraits(); - - foreach ($this->afterApplicationCreatedCallbacks as $callback) { - $callback(); - } - - Model::setEventDispatcher($this->app['events']); - - $this->setUpHasRun = true; + $this->setUpTheTestEnvironment(); } /** @@ -116,56 +50,6 @@ protected function refreshApplication() $this->app = $this->createApplication(); } - /** - * Boot the testing helper traits. - * - * @return array - */ - protected function setUpTraits() - { - $uses = array_flip(class_uses_recursive(static::class)); - - if (isset($uses[RefreshDatabase::class])) { - $this->refreshDatabase(); - } - - if (isset($uses[DatabaseMigrations::class])) { - $this->runDatabaseMigrations(); - } - - if (isset($uses[DatabaseTruncation::class])) { - $this->truncateDatabaseTables(); - } - - if (isset($uses[DatabaseTransactions::class])) { - $this->beginDatabaseTransaction(); - } - - if (isset($uses[WithoutMiddleware::class])) { - $this->disableMiddlewareForAllTests(); - } - - if (isset($uses[WithoutEvents::class])) { - $this->disableEventsForAllTests(); - } - - if (isset($uses[WithFaker::class])) { - $this->setUpFaker(); - } - - foreach ($uses as $trait) { - if (method_exists($this, $method = 'setUp'.class_basename($trait))) { - $this->{$method}(); - } - - if (method_exists($this, $method = 'tearDown'.class_basename($trait))) { - $this->beforeApplicationDestroyed(fn () => $this->{$method}()); - } - } - - return $uses; - } - /** * {@inheritdoc} */ @@ -195,68 +79,7 @@ protected function runTest(): mixed */ protected function tearDown(): void { - if ($this->app) { - $this->callBeforeApplicationDestroyedCallbacks(); - - ParallelTesting::callTearDownTestCaseCallbacks($this); - - $this->app->flush(); - - $this->app = null; - } - - $this->setUpHasRun = false; - - if (property_exists($this, 'serverVariables')) { - $this->serverVariables = []; - } - - if (property_exists($this, 'defaultHeaders')) { - $this->defaultHeaders = []; - } - - if (class_exists('Mockery')) { - if ($container = Mockery::getContainer()) { - $this->addToAssertionCount($container->mockery_getExpectationCount()); - } - - try { - Mockery::close(); - } catch (InvalidCountException $e) { - if (! Str::contains($e->getMethodName(), ['doWrite', 'askQuestion'])) { - throw $e; - } - } - } - - if (class_exists(Carbon::class)) { - Carbon::setTestNow(); - } - - if (class_exists(CarbonImmutable::class)) { - CarbonImmutable::setTestNow(); - } - - $this->afterApplicationCreatedCallbacks = []; - $this->beforeApplicationDestroyedCallbacks = []; - - $this->originalExceptionHandler = null; - $this->originalDeprecationHandler = null; - - AboutCommand::flushState(); - Artisan::forgetBootstrappers(); - Component::flushCache(); - Component::forgetComponentsResolver(); - Component::forgetFactory(); - ConvertEmptyStringsToNull::flushState(); - HandleExceptions::forgetApp(); - Queue::createPayloadUsing(null); - Sleep::fake(false); - TrimStrings::flushState(); - - if ($this->callbackException) { - throw $this->callbackException; - } + $this->tearDownTheTestEnvironment(); } /** @@ -268,60 +91,6 @@ public static function tearDownAfterClass(): void { static::$latestResponse = null; - foreach ([ - \PHPUnit\Util\Annotation\Registry::class, - \PHPUnit\Metadata\Annotation\Parser\Registry::class, - ] as $class) { - if (class_exists($class)) { - (function () { - $this->classDocBlocks = []; - $this->methodDocBlocks = []; - })->call($class::getInstance()); - } - } - } - - /** - * Register a callback to be run after the application is created. - * - * @param callable $callback - * @return void - */ - public function afterApplicationCreated(callable $callback) - { - $this->afterApplicationCreatedCallbacks[] = $callback; - - if ($this->setUpHasRun) { - $callback(); - } - } - - /** - * Register a callback to be run before the application is destroyed. - * - * @param callable $callback - * @return void - */ - protected function beforeApplicationDestroyed(callable $callback) - { - $this->beforeApplicationDestroyedCallbacks[] = $callback; - } - - /** - * Execute the application's pre-destruction callbacks. - * - * @return void - */ - protected function callBeforeApplicationDestroyedCallbacks() - { - foreach ($this->beforeApplicationDestroyedCallbacks as $callback) { - try { - $callback(); - } catch (Throwable $e) { - if (! $this->callbackException) { - $this->callbackException = $e; - } - } - } + static::tearDownAfterClassUsingTestCase(); } } diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index e4c15edca725..c698bbdfaa8e 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -929,7 +929,7 @@ function trans($key = null, $replace = [], $locale = null) * Translates the given message based on a count. * * @param string $key - * @param \Countable|int|array $number + * @param \Countable|int|float|array $number * @param array $replace * @param string|null $locale * @return string diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index d3411ce18777..4bcd13d21b09 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -36,6 +36,13 @@ class Factory */ protected $globalMiddleware = []; + /** + * The options to apply to every request. + * + * @var array + */ + protected $globalOptions = []; + /** * The stub callables that will handle requests. * @@ -123,6 +130,19 @@ public function globalResponseMiddleware($middleware) return $this; } + /** + * Set the options to apply to every request. + * + * @param array $options + * @return $this + */ + public function globalOptions($options) + { + $this->globalOptions = $options; + + return $this; + } + /** * Create a new response instance for use during stubbing. * @@ -400,7 +420,7 @@ public function recorded($callback = null) */ protected function newPendingRequest() { - return new PendingRequest($this, $this->globalMiddleware); + return (new PendingRequest($this, $this->globalMiddleware))->withOptions($this->globalOptions); } /** diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 78e361feffad..7eb8b0e47b6c 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -78,7 +78,7 @@ class PendingRequest /** * The raw body for the request. * - * @var string + * @var \Psr\Http\Message\StreamInterface|string */ protected $pendingBody; @@ -259,7 +259,7 @@ public function baseUrl(string $url) /** * Attach a raw body to the request. * - * @param string $content + * @param \Psr\Http\Message\StreamInterface|string $content * @param string $contentType * @return $this */ @@ -600,13 +600,13 @@ public function connectTimeout(int $seconds) /** * Specify the number of times the request should be attempted. * - * @param int $times + * @param array|int $times * @param Closure|int $sleepMilliseconds * @param callable|null $when * @param bool $throw * @return $this */ - public function retry(int $times, Closure|int $sleepMilliseconds = 0, ?callable $when = null, bool $throw = true) + public function retry(array|int $times, Closure|int $sleepMilliseconds = 0, ?callable $when = null, bool $throw = true) { $this->tries = $times; $this->retryDelay = $sleepMilliseconds; @@ -911,17 +911,21 @@ public function send(string $method, string $url, array $options = []) $response->throw($this->throwCallback); } - if ($attempt < $this->tries && $shouldRetry) { + $potentialTries = is_array($this->tries) + ? count($this->tries) + 1 + : $this->tries; + + if ($attempt < $potentialTries && $shouldRetry) { $response->throw(); } - if ($this->tries > 1 && $this->retryThrow) { + if ($potentialTries > 1 && $this->retryThrow) { $response->throw(); } } }); } catch (ConnectException $e) { - $this->dispatchConnectionFailedEvent(); + $this->dispatchConnectionFailedEvent(new Request($e->getRequest())); throw new ConnectionException($e->getMessage(), 0, $e); } @@ -1010,7 +1014,7 @@ protected function makePromise(string $method, string $url, array $options = []) }) ->otherwise(function (OutOfBoundsException|TransferException $e) { if ($e instanceof ConnectException) { - $this->dispatchConnectionFailedEvent(); + $this->dispatchConnectionFailedEvent(new Request($e->getRequest())); } return $e instanceof RequestException && $e->hasResponse() ? $this->populateResponse($this->newResponse($e->getResponse())) : $e; @@ -1405,8 +1409,7 @@ protected function dispatchRequestSendingEvent() */ protected function dispatchResponseReceivedEvent(Response $response) { - if (! ($dispatcher = $this->factory?->getDispatcher()) || - ! $this->request) { + if (! ($dispatcher = $this->factory?->getDispatcher()) || ! $this->request) { return; } @@ -1416,12 +1419,13 @@ protected function dispatchResponseReceivedEvent(Response $response) /** * Dispatch the ConnectionFailed event if a dispatcher is available. * + * @param \Illuminate\Http\Client\Request $request * @return void */ - protected function dispatchConnectionFailedEvent() + protected function dispatchConnectionFailedEvent(Request $request) { if ($dispatcher = $this->factory?->getDispatcher()) { - $dispatcher->dispatch(new ConnectionFailed($this->request)); + $dispatcher->dispatch(new ConnectionFailed($request)); } } diff --git a/src/Illuminate/Http/Exceptions/HttpResponseException.php b/src/Illuminate/Http/Exceptions/HttpResponseException.php index b27052f02c15..c45268680aeb 100644 --- a/src/Illuminate/Http/Exceptions/HttpResponseException.php +++ b/src/Illuminate/Http/Exceptions/HttpResponseException.php @@ -4,6 +4,7 @@ use RuntimeException; use Symfony\Component\HttpFoundation\Response; +use Throwable; class HttpResponseException extends RuntimeException { @@ -18,10 +19,13 @@ class HttpResponseException extends RuntimeException * Create a new HTTP response exception instance. * * @param \Symfony\Component\HttpFoundation\Response $response + * @param \Throwable $previous * @return void */ - public function __construct(Response $response) + public function __construct(Response $response, ?Throwable $previous = null) { + parent::__construct($previous?->getMessage() ?? '', $previous?->getCode() ?? 0, $previous); + $this->response = $response; } diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index 1a36ec2d0f1e..d7c8745f719b 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -404,7 +404,7 @@ public function get(string $key, mixed $default = null): mixed public function json($key = null, $default = null) { if (! isset($this->json)) { - $this->json = new InputBag((array) json_decode($this->getContent(), true)); + $this->json = new InputBag((array) json_decode($this->getContent() ?: '[]', true)); } if (is_null($key)) { diff --git a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php index 9940c3e0cea6..0f03ebda832b 100644 --- a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php +++ b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php @@ -266,15 +266,17 @@ protected function whenLoaded($relationship, $value = null, $default = null) return value($default); } + $loadedValue = $this->resource->{$relationship}; + if (func_num_args() === 1) { - return $this->resource->{$relationship}; + return $loadedValue; } - if ($this->resource->{$relationship} === null) { + if ($loadedValue === null) { return; } - return value($value); + return value($value, $loadedValue); } /** diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 0c2fd151ffbe..cf08343d7832 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -12,6 +12,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; use Illuminate\Http\Resources\DelegatesToResource; +use JsonException; use JsonSerializable; class JsonResource implements ArrayAccess, JsonSerializable, Responsable, UrlRoutable @@ -144,10 +145,10 @@ public function toArray(Request $request) */ public function toJson($options = 0) { - $json = json_encode($this->jsonSerialize(), $options); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw JsonEncodingException::forResource($this, json_last_error_msg()); + try { + $json = json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw JsonEncodingException::forResource($this, $e->getMessage()); } return $json; diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 15df806d55e6..b3ece35d811c 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -630,7 +630,7 @@ public function getChannels() /** * System is unusable. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -645,7 +645,7 @@ public function emergency($message, array $context = []): void * 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 */ @@ -659,7 +659,7 @@ public function alert($message, array $context = []): void * * Example: Application component unavailable, unexpected exception. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -672,7 +672,7 @@ public function critical($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 array $context * @return void */ @@ -687,7 +687,7 @@ public function error($message, array $context = []): void * 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 */ @@ -699,7 +699,7 @@ public function warning($message, array $context = []): void /** * Normal but significant events. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -713,7 +713,7 @@ public function notice($message, array $context = []): void * * Example: User logs in, SQL logs. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -725,7 +725,7 @@ public function info($message, array $context = []): void /** * Detailed debug information. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -738,7 +738,7 @@ public function debug($message, array $context = []): void * Logs with an arbitrary level. * * @param mixed $level - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index f821aa933e40..0c54fa23b316 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -259,7 +259,7 @@ protected function createSesTransport(array $config) * Create an instance of the Symfony Amazon SES V2 Transport driver. * * @param array $config - * @return \Illuminate\Mail\Transport\Se2VwTransport + * @return \Illuminate\Mail\Transport\SesV2Transport */ protected function createSesV2Transport(array $config) { diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 4016ee5a210e..48831c0e551a 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -347,7 +347,7 @@ public function buildViewData() } foreach ((new ReflectionClass($this))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { - if ($property->getDeclaringClass()->getName() !== self::class) { + if ($property->isInitialized($this) && $property->getDeclaringClass()->getName() !== self::class) { $data[$property->getName()] = $property->getValue($this); } } diff --git a/src/Illuminate/Mail/Mailables/Envelope.php b/src/Illuminate/Mail/Mailables/Envelope.php index 7d6c4b1ee55f..c5126f5ff2e4 100644 --- a/src/Illuminate/Mail/Mailables/Envelope.php +++ b/src/Illuminate/Mail/Mailables/Envelope.php @@ -77,10 +77,10 @@ class Envelope * Create a new message envelope instance. * * @param \Illuminate\Mail\Mailables\Address|string|null $from - * @param array $to - * @param array $cc - * @param array $bcc - * @param array $replyTo + * @param array $to + * @param array $cc + * @param array $bcc + * @param array $replyTo * @param string|null $subject * @param array $tags * @param array $metadata @@ -105,8 +105,8 @@ public function __construct(Address|string $from = null, $to = [], $cc = [], $bc /** * Normalize the given array of addresses. * - * @param array $addresses - * @return array + * @param array $addresses + * @return array */ protected function normalizeAddresses($addresses) { @@ -132,7 +132,7 @@ public function from(Address|string $address, $name = null) /** * Add a "to" recipient to the message envelope. * - * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param \Illuminate\Mail\Mailables\Address|array|string $address * @param string|null $name * @return $this */ @@ -148,7 +148,7 @@ public function to(Address|array|string $address, $name = null) /** * Add a "cc" recipient to the message envelope. * - * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param \Illuminate\Mail\Mailables\Address|array|string $address * @param string|null $name * @return $this */ @@ -164,7 +164,7 @@ public function cc(Address|array|string $address, $name = null) /** * Add a "bcc" recipient to the message envelope. * - * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param \Illuminate\Mail\Mailables\Address|array|string $address * @param string|null $name * @return $this */ @@ -180,7 +180,7 @@ public function bcc(Address|array|string $address, $name = null) /** * Add a "reply to" recipient to the message envelope. * - * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param \Illuminate\Mail\Mailables\Address|array|string $address * @param string|null $name * @return $this */ diff --git a/src/Illuminate/Mail/Transport/LogTransport.php b/src/Illuminate/Mail/Transport/LogTransport.php index 291251200a48..682169e3eaa7 100644 --- a/src/Illuminate/Mail/Transport/LogTransport.php +++ b/src/Illuminate/Mail/Transport/LogTransport.php @@ -2,6 +2,7 @@ namespace Illuminate\Mail\Transport; +use Illuminate\Support\Str; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\SentMessage; @@ -33,17 +34,48 @@ public function __construct(LoggerInterface $logger) */ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { - $string = $message->toString(); + $string = Str::of($message->toString()); - if (str_contains($string, 'Content-Transfer-Encoding: quoted-printable')) { - $string = quoted_printable_decode($string); + if ($string->contains('Content-Type: multipart/')) { + $boundary = $string + ->after('boundary=') + ->before("\r\n") + ->prepend('--') + ->append("\r\n"); + + $string = $string + ->explode($boundary) + ->map($this->decodeQuotedPrintableContent(...)) + ->implode($boundary); + } elseif ($string->contains('Content-Transfer-Encoding: quoted-printable')) { + $string = $this->decodeQuotedPrintableContent($string); } - $this->logger->debug($string); + $this->logger->debug((string) $string); return new SentMessage($message, $envelope ?? Envelope::create($message)); } + /** + * Decode the given quoted printable content. + * + * @param string $part + * @return string + */ + protected function decodeQuotedPrintableContent(string $part) + { + if (! str_contains($part, 'Content-Transfer-Encoding: quoted-printable')) { + return $part; + } + + [$headers, $content] = explode("\r\n\r\n", $part, 2); + + return implode("\r\n\r\n", [ + $headers, + quoted_printable_decode($content), + ]); + } + /** * Get the logger for the LogTransport instance. * diff --git a/src/Illuminate/Mail/Transport/SesV2Transport.php b/src/Illuminate/Mail/Transport/SesV2Transport.php index 876630b9e1be..feb25d61a493 100644 --- a/src/Illuminate/Mail/Transport/SesV2Transport.php +++ b/src/Illuminate/Mail/Transport/SesV2Transport.php @@ -51,7 +51,7 @@ protected function doSend(SentMessage $message): void if ($message->getOriginalMessage() instanceof Message) { foreach ($message->getOriginalMessage()->getHeaders()->all() as $header) { if ($header instanceof MetadataHeader) { - $options['Tags'][] = ['Name' => $header->getKey(), 'Value' => $header->getValue()]; + $options['EmailTags'][] = ['Name' => $header->getKey(), 'Value' => $header->getValue()]; } } } diff --git a/src/Illuminate/Mail/resources/views/text/header.blade.php b/src/Illuminate/Mail/resources/views/text/header.blade.php index aaa3e5754446..97444ebdcfd1 100644 --- a/src/Illuminate/Mail/resources/views/text/header.blade.php +++ b/src/Illuminate/Mail/resources/views/text/header.blade.php @@ -1 +1 @@ -[{{ $slot }}]({{ $url }}) +{{ $slot }}: {{ $url }} diff --git a/src/Illuminate/Pagination/AbstractCursorPaginator.php b/src/Illuminate/Pagination/AbstractCursorPaginator.php index e6700789c176..fa3070c24521 100644 --- a/src/Illuminate/Pagination/AbstractCursorPaginator.php +++ b/src/Illuminate/Pagination/AbstractCursorPaginator.php @@ -205,6 +205,7 @@ public function getCursorForItem($item, $isNext = true) public function getParametersForItem($item) { return collect($this->parameters) + ->filter() ->flip() ->map(function ($_, $parameterName) use ($item) { if ($item instanceof JsonResource) { diff --git a/src/Illuminate/Process/PendingProcess.php b/src/Illuminate/Process/PendingProcess.php index 320172bd266c..53ffdd130def 100644 --- a/src/Illuminate/Process/PendingProcess.php +++ b/src/Illuminate/Process/PendingProcess.php @@ -266,8 +266,10 @@ public function run(array|string $command = null, callable $output = null) * Start the process in the background. * * @param array|string|null $command - * @param callable $output + * @param callable|null $output * @return \Illuminate\Process\InvokedProcess + * + * @throws \RuntimeException */ public function start(array|string $command = null, callable $output = null) { @@ -348,7 +350,7 @@ public function withFakeHandlers(array $fakeHandlers) protected function fakeFor(string $command) { return collect($this->fakeHandlers) - ->first(fn ($handler, $pattern) => Str::is($pattern, $command)); + ->first(fn ($handler, $pattern) => $pattern === '*' || Str::is($pattern, $command)); } /** @@ -382,6 +384,8 @@ protected function resolveSynchronousFake(string $command, Closure $fake) * @param callable|null $output * @param \Closure $fake * @return \Illuminate\Process\FakeInvokedProcess + * + * @throw \LogicException */ protected function resolveAsynchronousFake(string $command, ?callable $output, Closure $fake) { diff --git a/src/Illuminate/Queue/Console/ClearCommand.php b/src/Illuminate/Queue/Console/ClearCommand.php index 6f3e8dc3bf9d..8f4187bcac77 100644 --- a/src/Illuminate/Queue/Console/ClearCommand.php +++ b/src/Illuminate/Queue/Console/ClearCommand.php @@ -57,6 +57,8 @@ public function handle() $this->components->info('Cleared '.$count.' '.Str::plural('job', $count).' from the ['.$queueName.'] queue'); } else { $this->components->error('Clearing queues is not supported on ['.(new ReflectionClass($queue))->getShortName().']'); + + return 1; } return 0; diff --git a/src/Illuminate/Queue/Console/ForgetFailedCommand.php b/src/Illuminate/Queue/Console/ForgetFailedCommand.php index 22d87d32b128..fce7803ccda7 100644 --- a/src/Illuminate/Queue/Console/ForgetFailedCommand.php +++ b/src/Illuminate/Queue/Console/ForgetFailedCommand.php @@ -25,7 +25,7 @@ class ForgetFailedCommand extends Command /** * Execute the console command. * - * @return void + * @return int|null */ public function handle() { @@ -33,6 +33,8 @@ public function handle() $this->components->info('Failed job deleted successfully.'); } else { $this->components->error('No failed job matches the given ID.'); + + return 1; } } } diff --git a/src/Illuminate/Queue/Events/JobQueueing.php b/src/Illuminate/Queue/Events/JobQueueing.php new file mode 100644 index 000000000000..ebb0769681b4 --- /dev/null +++ b/src/Illuminate/Queue/Events/JobQueueing.php @@ -0,0 +1,58 @@ +connectionName = $connectionName; + $this->job = $job; + $this->payload = $payload; + } + + /** + * Get the decoded job payload. + * + * @return array + */ + public function payload() + { + if ($this->payload === null) { + throw new RuntimeException('The job payload was not provided when the event was dispatched.'); + } + + return json_decode($this->payload, true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/src/Illuminate/Queue/Listener.php b/src/Illuminate/Queue/Listener.php index b552828f167f..f7744b45bb01 100755 --- a/src/Illuminate/Queue/Listener.php +++ b/src/Illuminate/Queue/Listener.php @@ -216,7 +216,7 @@ public function memoryExceeded($memoryLimit) /** * Stop listening and bail out of the script. * - * @return void + * @return never */ public function stop() { diff --git a/src/Illuminate/Queue/Queue.php b/src/Illuminate/Queue/Queue.php index 1aa09ee30bdb..09eb24526311 100755 --- a/src/Illuminate/Queue/Queue.php +++ b/src/Illuminate/Queue/Queue.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; use Illuminate\Queue\Events\JobQueued; +use Illuminate\Queue\Events\JobQueueing; use Illuminate\Support\Arr; use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; @@ -327,6 +328,8 @@ protected function enqueueUsing($job, $payload, $queue, $delay, $callback) $this->container->bound('db.transactions')) { return $this->container->make('db.transactions')->addCallback( function () use ($payload, $queue, $delay, $callback, $job) { + $this->raiseJobQueueingEvent($job, $payload); + return tap($callback($payload, $queue, $delay), function ($jobId) use ($job, $payload) { $this->raiseJobQueuedEvent($jobId, $job, $payload); }); @@ -334,6 +337,8 @@ function () use ($payload, $queue, $delay, $callback, $job) { ); } + $this->raiseJobQueueingEvent($job, $payload); + return tap($callback($payload, $queue, $delay), function ($jobId) use ($job, $payload) { $this->raiseJobQueuedEvent($jobId, $job, $payload); }); @@ -362,6 +367,20 @@ protected function shouldDispatchAfterCommit($job) return false; } + /** + * Raise the job queueing event. + * + * @param \Closure|string|object $job + * @param string $payload + * @return void + */ + protected function raiseJobQueueingEvent($job, $payload) + { + if ($this->container->bound('events')) { + $this->container['events']->dispatch(new JobQueueing($this->connectionName, $job, $payload)); + } + } + /** * Raise the job queued event. * diff --git a/src/Illuminate/Routing/ImplicitRouteBinding.php b/src/Illuminate/Routing/ImplicitRouteBinding.php index d3590de1d707..f8352e3d57e1 100644 --- a/src/Illuminate/Routing/ImplicitRouteBinding.php +++ b/src/Illuminate/Routing/ImplicitRouteBinding.php @@ -86,7 +86,9 @@ protected static function resolveBackedEnumsForRoute($route, $parameters) $backedEnumClass = $parameter->getType()?->getName(); - $backedEnum = $backedEnumClass::tryFrom((string) $parameterValue); + $backedEnum = $parameterValue instanceof $backedEnumClass + ? $parameterValue + : $backedEnumClass::tryFrom((string) $parameterValue); if (is_null($backedEnum)) { throw new BackedEnumCaseNotFoundException($backedEnumClass, $parameterValue); diff --git a/src/Illuminate/Routing/ResponseFactory.php b/src/Illuminate/Routing/ResponseFactory.php index 35b209584549..84c69225e370 100644 --- a/src/Illuminate/Routing/ResponseFactory.php +++ b/src/Illuminate/Routing/ResponseFactory.php @@ -10,6 +10,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; use Symfony\Component\HttpFoundation\StreamedResponse; use Throwable; @@ -129,6 +130,20 @@ public function stream($callback, $status = 200, array $headers = []) return new StreamedResponse($callback, $status, $headers); } + /** + * Create a new streamed response instance. + * + * @param array $data + * @param int $status + * @param array $headers + * @param int $encodingOptions + * @return \Symfony\Component\HttpFoundation\StreamedJsonResponse + */ + public function streamJson($data, $status = 200, $headers = [], $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS) + { + return new StreamedJsonResponse($data, $status, $headers, $encodingOptions); + } + /** * Create a new streamed response instance as a file download. * diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index 02518733300c..86e808a9446f 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -577,7 +577,7 @@ public function setBindingFields(array $bindingFields) * Get the parent parameter of the given parameter. * * @param string $parameter - * @return string + * @return string|null */ public function parentOfParameter($parameter) { diff --git a/src/Illuminate/Routing/UrlGenerator.php b/src/Illuminate/Routing/UrlGenerator.php index 0b9dcec33700..74c40d87cbbd 100755 --- a/src/Illuminate/Routing/UrlGenerator.php +++ b/src/Illuminate/Routing/UrlGenerator.php @@ -194,9 +194,7 @@ public function previousPath($fallback = false) */ protected function getPreviousUrlFromSession() { - $session = $this->getSession(); - - return $session ? $session->previousUrl() : null; + return $this->getSession()?->previousUrl(); } /** diff --git a/src/Illuminate/Support/Carbon.php b/src/Illuminate/Support/Carbon.php index c1a665c7d0f8..343918991a23 100644 --- a/src/Illuminate/Support/Carbon.php +++ b/src/Illuminate/Support/Carbon.php @@ -29,9 +29,11 @@ public static function setTestNow($testNow = null) */ public static function createFromId($id) { - return Ulid::isValid($id) - ? static::createFromInterface(Ulid::fromString($id)->getDateTime()) - : static::createFromInterface(Uuid::fromString($id)->getDateTime()); + if (is_string($id)) { + $id = Ulid::isValid($id) ? Ulid::fromString($id) : Uuid::fromString($id); + } + + return static::createFromInterface($id->getDateTime()); } /** diff --git a/src/Illuminate/Support/DefaultProviders.php b/src/Illuminate/Support/DefaultProviders.php index ef7422fd6239..395b7cb9ec43 100644 --- a/src/Illuminate/Support/DefaultProviders.php +++ b/src/Illuminate/Support/DefaultProviders.php @@ -33,10 +33,10 @@ public function __construct(?array $providers = null) \Illuminate\Mail\MailServiceProvider::class, \Illuminate\Notifications\NotificationServiceProvider::class, \Illuminate\Pagination\PaginationServiceProvider::class, + \Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, \Illuminate\Pipeline\PipelineServiceProvider::class, \Illuminate\Queue\QueueServiceProvider::class, \Illuminate\Redis\RedisServiceProvider::class, - \Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, \Illuminate\Session\SessionServiceProvider::class, \Illuminate\Translation\TranslationServiceProvider::class, \Illuminate\Validation\ValidationServiceProvider::class, diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index e13bd509c9fb..a132a47aa0c8 100755 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -52,6 +52,7 @@ * @method static float totalQueryDuration() * @method static void resetTotalQueryDuration() * @method static void reconnectIfMissingConnection() + * @method static \Illuminate\Database\Connection beforeStartingTransaction(\Closure $callback) * @method static \Illuminate\Database\Connection beforeExecuting(\Closure $callback) * @method static void listen(\Closure $callback) * @method static \Illuminate\Contracts\Database\Query\Expression raw(mixed $value) diff --git a/src/Illuminate/Support/Facades/Facade.php b/src/Illuminate/Support/Facades/Facade.php index 1dbdc321543b..2dbf100cd5d1 100755 --- a/src/Illuminate/Support/Facades/Facade.php +++ b/src/Illuminate/Support/Facades/Facade.php @@ -18,7 +18,7 @@ abstract class Facade /** * The application instance being facaded. * - * @var \Illuminate\Contracts\Foundation\Application + * @var \Illuminate\Contracts\Foundation\Application|null */ protected static $app; @@ -317,7 +317,7 @@ public static function defaultAliases() /** * Get the application instance behind the facade. * - * @return \Illuminate\Contracts\Foundation\Application + * @return \Illuminate\Contracts\Foundation\Application|null */ public static function getFacadeApplication() { @@ -327,7 +327,7 @@ public static function getFacadeApplication() /** * Set the application instance. * - * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Illuminate\Contracts\Foundation\Application|null $app * @return void */ public static function setFacadeApplication($app) diff --git a/src/Illuminate/Support/Facades/File.php b/src/Illuminate/Support/Facades/File.php index a1ade8789643..fd43b658e387 100755 --- a/src/Illuminate/Support/Facades/File.php +++ b/src/Illuminate/Support/Facades/File.php @@ -21,7 +21,7 @@ * @method static bool delete(string|array $paths) * @method static bool move(string $path, string $target) * @method static bool copy(string $path, string $target) - * @method static void link(string $target, string $link) + * @method static bool|null link(string $target, string $link) * @method static void relativeLink(string $target, string $link) * @method static string name(string $path) * @method static string basename(string $path) diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 6a445e078055..c472d699e99c 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -8,6 +8,7 @@ * @method static \Illuminate\Http\Client\Factory globalMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) + * @method static \Illuminate\Http\Client\Factory globalOptions(array $options) * @method static \GuzzleHttp\Promise\PromiseInterface response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \Illuminate\Http\Client\ResponseSequence sequence(array $responses = []) * @method static \Illuminate\Http\Client\Factory allowStrayRequests() @@ -27,7 +28,7 @@ * @method static void flushMacros() * @method static mixed macroCall(string $method, array $parameters) * @method static \Illuminate\Http\Client\PendingRequest baseUrl(string $url) - * @method static \Illuminate\Http\Client\PendingRequest withBody(string $content, string $contentType = 'application/json') + * @method static \Illuminate\Http\Client\PendingRequest withBody(\Psr\Http\Message\StreamInterface|string $content, string $contentType = 'application/json') * @method static \Illuminate\Http\Client\PendingRequest asJson() * @method static \Illuminate\Http\Client\PendingRequest asForm() * @method static \Illuminate\Http\Client\PendingRequest attach(string|array $name, string|resource $contents = '', string|null $filename = null, array $headers = []) @@ -52,7 +53,7 @@ * @method static \Illuminate\Http\Client\PendingRequest sink(string|resource $to) * @method static \Illuminate\Http\Client\PendingRequest timeout(int $seconds) * @method static \Illuminate\Http\Client\PendingRequest connectTimeout(int $seconds) - * @method static \Illuminate\Http\Client\PendingRequest retry(int $times, \Closure|int $sleepMilliseconds = 0, callable|null $when = null, bool $throw = true) + * @method static \Illuminate\Http\Client\PendingRequest retry(array|int $times, \Closure|int $sleepMilliseconds = 0, callable|null $when = null, bool $throw = true) * @method static \Illuminate\Http\Client\PendingRequest withOptions(array $options) * @method static \Illuminate\Http\Client\PendingRequest withMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\PendingRequest withRequestMiddleware(callable $middleware) diff --git a/src/Illuminate/Support/Facades/Lang.php b/src/Illuminate/Support/Facades/Lang.php index cdaad3d0fd16..a341b5fab640 100755 --- a/src/Illuminate/Support/Facades/Lang.php +++ b/src/Illuminate/Support/Facades/Lang.php @@ -6,7 +6,7 @@ * @method static bool hasForLocale(string $key, string|null $locale = null) * @method static bool has(string $key, string|null $locale = null, bool $fallback = true) * @method static string|array get(string $key, array $replace = [], string|null $locale = null, bool $fallback = true) - * @method static string choice(string $key, \Countable|int|array $number, array $replace = [], string|null $locale = null) + * @method static string choice(string $key, \Countable|int|float|array $number, array $replace = [], string|null $locale = null) * @method static void addLines(array $lines, string $locale, string $namespace = '*') * @method static void load(string $namespace, string $group, string $locale) * @method static \Illuminate\Translation\Translator handleMissingKeysUsing(callable|null $callback) diff --git a/src/Illuminate/Support/Facades/Log.php b/src/Illuminate/Support/Facades/Log.php index 37e03961ebd5..ba40965286fe 100755 --- a/src/Illuminate/Support/Facades/Log.php +++ b/src/Illuminate/Support/Facades/Log.php @@ -16,15 +16,15 @@ * @method static \Illuminate\Log\LogManager extend(string $driver, \Closure $callback) * @method static void forgetChannel(string|null $driver = null) * @method static array getChannels() - * @method static void emergency(string $message, array $context = []) - * @method static void alert(string $message, array $context = []) - * @method static void critical(string $message, array $context = []) - * @method static void error(string $message, array $context = []) - * @method static void warning(string $message, array $context = []) - * @method static void notice(string $message, array $context = []) - * @method static void info(string $message, array $context = []) - * @method static void debug(string $message, array $context = []) - * @method static void log(mixed $level, string $message, array $context = []) + * @method static void emergency(string|\Stringable $message, array $context = []) + * @method static void alert(string|\Stringable $message, array $context = []) + * @method static void critical(string|\Stringable $message, array $context = []) + * @method static void error(string|\Stringable $message, array $context = []) + * @method static void warning(string|\Stringable $message, array $context = []) + * @method static void notice(string|\Stringable $message, array $context = []) + * @method static void info(string|\Stringable $message, array $context = []) + * @method static void debug(string|\Stringable $message, array $context = []) + * @method static void log(mixed $level, string|\Stringable $message, array $context = []) * @method static void write(string $level, \Illuminate\Contracts\Support\Arrayable|\Illuminate\Contracts\Support\Jsonable|\Illuminate\Support\Stringable|array|string $message, array $context = []) * @method static \Illuminate\Log\Logger withContext(array $context = []) * @method static void listen(\Closure $callback) diff --git a/src/Illuminate/Support/Facades/Notification.php b/src/Illuminate/Support/Facades/Notification.php index e82aa0529d65..8b30997e7923 100644 --- a/src/Illuminate/Support/Facades/Notification.php +++ b/src/Illuminate/Support/Facades/Notification.php @@ -31,6 +31,7 @@ * @method static void assertCount(int $expectedCount) * @method static \Illuminate\Support\Collection sent(mixed $notifiable, string $notification, callable|null $callback = null) * @method static bool hasSent(mixed $notifiable, string $notification) + * @method static \Illuminate\Support\Testing\Fakes\NotificationFake serializeAndRestore(bool $serializeAndRestore = true) * @method static array sentNotifications() * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) @@ -54,6 +55,23 @@ public static function fake() }); } + /** + * Begin sending a notification to an anonymous notifiable on the given channels. + * + * @param array $channels + * @return \Illuminate\Notifications\AnonymousNotifiable + */ + public static function routes(array $channels) + { + $notifiable = new AnonymousNotifiable; + + foreach ($channels as $channel => $route) { + $notifiable->route($channel, $route); + } + + return $notifiable; + } + /** * Begin sending a notification to an anonymous notifiable. * diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index 1df17ba7e7a9..43b5b93a6578 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -17,7 +17,7 @@ * @method static \Illuminate\Process\PendingProcess tty(bool $tty = true) * @method static \Illuminate\Process\PendingProcess options(array $options) * @method static \Illuminate\Contracts\Process\ProcessResult run(array|string|null $command = null, callable|null $output = null) - * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable $output = null) + * @method static \Illuminate\Process\InvokedProcess start(array|string|null $command = null, callable|null $output = null) * @method static \Illuminate\Process\PendingProcess withFakeHandlers(array $fakeHandlers) * @method static \Illuminate\Process\PendingProcess|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) * @method static \Illuminate\Process\PendingProcess|mixed unless(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) diff --git a/src/Illuminate/Support/Facades/RateLimiter.php b/src/Illuminate/Support/Facades/RateLimiter.php index 5ac2e5b36dd0..e8b3ab3fe4f5 100644 --- a/src/Illuminate/Support/Facades/RateLimiter.php +++ b/src/Illuminate/Support/Facades/RateLimiter.php @@ -8,6 +8,7 @@ * @method static mixed attempt(string $key, int $maxAttempts, \Closure $callback, int $decaySeconds = 60) * @method static bool tooManyAttempts(string $key, int $maxAttempts) * @method static int hit(string $key, int $decaySeconds = 60) + * @method static int increment(string $key, int $decaySeconds = 60, int $amount = 1) * @method static mixed attempts(string $key) * @method static mixed resetAttempts(string $key) * @method static int remaining(string $key, int $maxAttempts) diff --git a/src/Illuminate/Support/Facades/Response.php b/src/Illuminate/Support/Facades/Response.php index b92259ddeb76..a5addbfaa6bf 100755 --- a/src/Illuminate/Support/Facades/Response.php +++ b/src/Illuminate/Support/Facades/Response.php @@ -11,6 +11,7 @@ * @method static \Illuminate\Http\JsonResponse json(mixed $data = [], int $status = 200, array $headers = [], int $options = 0) * @method static \Illuminate\Http\JsonResponse jsonp(string $callback, mixed $data = [], int $status = 200, array $headers = [], int $options = 0) * @method static \Symfony\Component\HttpFoundation\StreamedResponse stream(callable $callback, int $status = 200, array $headers = []) + * @method static \Symfony\Component\HttpFoundation\StreamedJsonResponse streamJson(array $data, int $status = 200, array $headers = [], int $encodingOptions = 15) * @method static \Symfony\Component\HttpFoundation\StreamedResponse streamDownload(callable $callback, string|null $name = null, array $headers = [], string|null $disposition = 'attachment') * @method static \Symfony\Component\HttpFoundation\BinaryFileResponse download(\SplFileInfo|string $file, string|null $name = null, array $headers = [], string|null $disposition = 'attachment') * @method static \Symfony\Component\HttpFoundation\BinaryFileResponse file(\SplFileInfo|string $file, array $headers = []) diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 8aa0eb900a55..b59fbf582ce9 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -13,6 +13,7 @@ * @method static bool hasTable(string $table) * @method static bool hasView(string $view) * @method static array getTables() + * @method static array getTableListing() * @method static array getViews() * @method static array getTypes() * @method static bool hasColumn(string $table, string $column) @@ -23,6 +24,8 @@ * @method static array getColumnListing(string $table) * @method static array getColumns(string $table) * @method static array getIndexes(string $table) + * @method static array getIndexListing(string $table) + * @method static bool hasIndex(string $table, string|array $index, string|null $type = null) * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php index cd32d7f479c5..54cefe6334b9 100644 --- a/src/Illuminate/Support/Sleep.php +++ b/src/Illuminate/Support/Sleep.php @@ -2,7 +2,6 @@ namespace Illuminate\Support; -use Carbon\Carbon; use Carbon\CarbonInterval; use DateInterval; use Illuminate\Support\Traits\Macroable; @@ -20,6 +19,13 @@ class Sleep */ public static $fakeSleepCallbacks = []; + /** + * Keep Carbon's "now" in sync when sleeping. + * + * @var bool + */ + protected static $syncWithCarbon = false; + /** * The total duration to sleep. * @@ -259,6 +265,10 @@ public function __destruct() if (static::$fake) { static::$sequence[] = $this->duration; + if (static::$syncWithCarbon) { + Carbon::setTestNow(Carbon::now()->add($this->duration)); + } + foreach (static::$fakeSleepCallbacks as $callback) { $callback($this->duration); } @@ -309,14 +319,16 @@ protected function pullPending() * Stay awake and capture any attempts to sleep. * * @param bool $value + * @param bool $syncWithCarbon * @return void */ - public static function fake($value = true) + public static function fake($value = true, $syncWithCarbon = false) { static::$fake = $value; static::$sequence = []; static::$fakeSleepCallbacks = []; + static::$syncWithCarbon = $syncWithCarbon; } /** @@ -458,4 +470,14 @@ public static function whenFakingSleep($callback) { static::$fakeSleepCallbacks[] = $callback; } + + /** + * Indicate that Carbon's "now" should be kept in sync when sleeping. + * + * @return void + */ + public static function syncWithCarbon($value = true) + { + static::$syncWithCarbon = $value; + } } diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 4d51462f6d65..6217bb33ef82 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -390,6 +390,27 @@ public static function wrap($value, $before, $after = null) return $before.$value.($after ??= $before); } + /** + * Unwrap the string with the given strings. + * + * @param string $value + * @param string $before + * @param string|null $after + * @return string + */ + public static function unwrap($value, $before, $after = null) + { + if (static::startsWith($value, $before)) { + $value = static::substr($value, static::length($before)); + } + + if (static::endsWith($value, $after ??= $before)) { + $value = static::substr($value, 0, -static::length($after)); + } + + return $value; + } + /** * Determine if a given string matches a given pattern. * @@ -521,7 +542,7 @@ public static function isUuid($value) return false; } - return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0; + return preg_match('/^[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}$/D', $value) > 0; } /** @@ -1167,7 +1188,7 @@ public static function replaceEnd($search, $replace, $subject) /** * Replace the patterns matching the given regular expression. * - * @param string $pattern + * @param array|string $pattern * @param \Closure|string $replace * @param array|string $subject * @param int $limit @@ -1277,17 +1298,20 @@ public static function headline($value) */ public static function apa($value) { + if (trim($value) === '') { + return $value; + } + $minorWords = [ 'and', 'as', 'but', 'for', 'if', 'nor', 'or', 'so', 'yet', 'a', 'an', 'the', 'at', 'by', 'for', 'in', 'of', 'off', 'on', 'per', 'to', 'up', 'via', + 'et', 'ou', 'un', 'une', 'la', 'le', 'les', 'de', 'du', 'des', 'par', 'à', ]; $endPunctuation = ['.', '!', '?', ':', '—', ',']; $words = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); - $words[0] = ucfirst(mb_strtolower($words[0])); - for ($i = 0; $i < count($words); $i++) { $lowercaseWord = mb_strtolower($words[$i]); @@ -1295,7 +1319,9 @@ public static function apa($value) $hyphenatedWords = explode('-', $lowercaseWord); $hyphenatedWords = array_map(function ($part) use ($minorWords) { - return (in_array($part, $minorWords) && mb_strlen($part) <= 3) ? $part : ucfirst($part); + return (in_array($part, $minorWords) && mb_strlen($part) <= 3) + ? $part + : mb_strtoupper(mb_substr($part, 0, 1)).mb_substr($part, 1); }, $hyphenatedWords); $words[$i] = implode('-', $hyphenatedWords); @@ -1305,7 +1331,7 @@ public static function apa($value) ! ($i === 0 || in_array(mb_substr($words[$i - 1], -1), $endPunctuation))) { $words[$i] = $lowercaseWord; } else { - $words[$i] = ucfirst($lowercaseWord); + $words[$i] = mb_strtoupper(mb_substr($lowercaseWord, 0, 1)).mb_substr($lowercaseWord, 1); } } } @@ -1514,6 +1540,29 @@ public static function take($string, int $limit): string return static::substr($string, 0, $limit); } + /** + * Convert the given string to Base64 encoding. + * + * @param string $string + * @return string + */ + public static function toBase64($string): string + { + return base64_encode($string); + } + + /** + * Decode the given Base64 encoded string. + * + * @param string $string + * @param bool $strict + * @return string|false + */ + public static function fromBase64($string, $strict = false) + { + return base64_decode($string, $strict); + } + /** * Make a string's first character lowercase. * @@ -1586,7 +1635,7 @@ public static function uuid() } /** - * Generate a time-ordered UUID (version 4). + * Generate a time-ordered UUID. * * @return \Ramsey\Uuid\UuidInterface */ diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index c89abb01e41c..3a37ff11ed58 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -204,7 +204,7 @@ public function containsAll($needles, $ignoreCase = false) * Convert the case of a string. * * @param int $mode - * @param string $encoding + * @param string|null $encoding * @return static */ public function convertCase(int $mode = MB_CASE_FOLD, ?string $encoding = 'UTF-8') @@ -720,7 +720,7 @@ public function replaceEnd($search, $replace) /** * Replace the patterns matching the given regular expression. * - * @param string $pattern + * @param array|string $pattern * @param \Closure|string $replace * @param int $limit * @return static @@ -769,7 +769,7 @@ public function start($prefix) /** * Strip HTML and PHP tags from the given string. * - * @param string $allowedTags + * @param string[]|string|null $allowedTags * @return static */ public function stripTags($allowedTags = null) @@ -1224,6 +1224,18 @@ public function wrap($before, $after = null) return new static(Str::wrap($this->value, $before, $after)); } + /** + * Unwrap the string with the given strings. + * + * @param string $before + * @param string|null $after + * @return static + */ + public function unwrap($before, $after = null) + { + return new static(Str::unwrap($this->value, $before, $after)); + } + /** * Convert the string into a `HtmlString` instance. * @@ -1234,6 +1246,27 @@ public function toHtmlString() return new HtmlString($this->value); } + /** + * Convert the string to Base64 encoding. + * + * @return static + */ + public function toBase64() + { + return new static(base64_encode($this->value)); + } + + /** + * Decode the Base64 encoded string. + * + * @param bool $strict + * @return static + */ + public function fromBase64($strict = false) + { + return new static(base64_decode($this->value, $strict)); + } + /** * Dump the string. * @@ -1281,11 +1314,12 @@ public function toString() /** * Get the underlying string value as an integer. * + * @param int $base * @return int */ - public function toInteger() + public function toInteger($base = 10) { - return intval($this->value); + return intval($this->value, $base); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php index df00c465ff3c..ee610a401069 100644 --- a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php +++ b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php @@ -6,6 +6,7 @@ use Exception; use Illuminate\Contracts\Notifications\Dispatcher as NotificationDispatcher; use Illuminate\Contracts\Notifications\Factory as NotificationFactory; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Notifications\AnonymousNotifiable; use Illuminate\Support\Collection; @@ -32,6 +33,13 @@ class NotificationFake implements Fake, NotificationDispatcher, NotificationFact */ public $locale; + /** + * Indicates if notifications should be serialized and restored when pushed to the queue. + * + * @var bool + */ + protected $serializeAndRestore = false; + /** * Assert if a notification was sent on-demand based on a truth-test callback. * @@ -313,7 +321,9 @@ public function sendNow($notifiables, $notification, array $channels = null) } $this->notifications[get_class($notifiable)][$notifiable->getKey()][get_class($notification)][] = [ - 'notification' => $notification, + 'notification' => $this->serializeAndRestore && $notification instanceof ShouldQueue + ? $this->serializeAndRestoreNotification($notification) + : $notification, 'channels' => $notifiableChannels, 'notifiable' => $notifiable, 'locale' => $notification->locale ?? $this->locale ?? value(function () use ($notifiable) { @@ -349,6 +359,30 @@ public function locale($locale) return $this; } + /** + * Specify if notification should be serialized and restored when being "pushed" to the queue. + * + * @param bool $serializeAndRestore + * @return $this + */ + public function serializeAndRestore(bool $serializeAndRestore = true) + { + $this->serializeAndRestore = $serializeAndRestore; + + return $this; + } + + /** + * Serialize and unserialize the notification to simulate the queueing process. + * + * @param mixed $notification + * @return mixed + */ + protected function serializeAndRestoreNotification($notification) + { + return unserialize(serialize($notification)); + } + /** * Get the notifications that have been sent. * diff --git a/src/Illuminate/Support/ValidatedInput.php b/src/Illuminate/Support/ValidatedInput.php index 87e47b55226c..70bc6c3d2e8c 100644 --- a/src/Illuminate/Support/ValidatedInput.php +++ b/src/Illuminate/Support/ValidatedInput.php @@ -4,7 +4,9 @@ use ArrayIterator; use Illuminate\Contracts\Support\ValidatedData; +use Illuminate\Support\Facades\Date; use stdClass; +use Symfony\Component\VarDumper\VarDumper; use Traversable; class ValidatedInput implements ValidatedData @@ -38,7 +40,7 @@ public function has($keys) $keys = is_array($keys) ? $keys : func_get_args(); foreach ($keys as $key) { - if (! Arr::has($this->input, $key)) { + if (! Arr::has($this->all(), $key)) { return false; } } @@ -67,7 +69,7 @@ public function only($keys) { $results = []; - $input = $this->input; + $input = $this->all(); $placeholder = new stdClass; @@ -92,7 +94,7 @@ public function except($keys) { $keys = is_array($keys) ? $keys : func_get_args(); - $results = $this->input; + $results = $this->all(); Arr::forget($results, $keys); @@ -107,17 +109,18 @@ public function except($keys) */ public function merge(array $items) { - return new static(array_merge($this->input, $items)); + return new static(array_merge($this->all(), $items)); } /** * Get the input as a collection. * + * @param array|string|null $key * @return \Illuminate\Support\Collection */ - public function collect() + public function collect($key = null) { - return new Collection($this->input); + return collect(is_array($key) ? $this->only($key) : $this->input($key)); } /** @@ -148,7 +151,7 @@ public function toArray() */ public function __get($name) { - return $this->input[$name]; + return $this->input($name); } /** @@ -170,7 +173,7 @@ public function __set($name, $value) */ public function __isset($name) { - return isset($this->input[$name]); + return $this->exists($name); } /** @@ -192,7 +195,7 @@ public function __unset($name) */ public function offsetExists($key): bool { - return isset($this->input[$key]); + return $this->exists($key); } /** @@ -203,7 +206,7 @@ public function offsetExists($key): bool */ public function offsetGet($key): mixed { - return $this->input[$key]; + return $this->input($key); } /** @@ -242,4 +245,320 @@ public function getIterator(): Traversable { return new ArrayIterator($this->input); } + + /** + * Determine if the validated inputs contains a given input item key. + * + * @param string|array $key + * @return bool + */ + public function exists($key) + { + return $this->has($key); + } + + /** + * Determine if the validated inputs contains any of the given inputs. + * + * @param string|array $keys + * @return bool + */ + public function hasAny($keys) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + $input = $this->all(); + + return Arr::hasAny($input, $keys); + } + + /** + * Apply the callback if the validated inputs contains the given input item key. + * + * @param string $key + * @param callable $callback + * @param callable|null $default + * @return $this|mixed + */ + public function whenHas($key, callable $callback, callable $default = null) + { + if ($this->has($key)) { + return $callback(data_get($this->all(), $key)) ?: $this; + } + + if ($default) { + return $default(); + } + + return $this; + } + + /** + * Determine if the validated inputs contains a non-empty value for an input item. + * + * @param string|array $key + * @return bool + */ + public function filled($key) + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $value) { + if ($this->isEmptyString($value)) { + return false; + } + } + + return true; + } + + /** + * Determine if the validated inputs contains an empty value for an input item. + * + * @param string|array $key + * @return bool + */ + public function isNotFilled($key) + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $value) { + if (! $this->isEmptyString($value)) { + return false; + } + } + + return true; + } + + /** + * Determine if the validated inputs contains a non-empty value for any of the given inputs. + * + * @param string|array $keys + * @return bool + */ + public function anyFilled($keys) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + foreach ($keys as $key) { + if ($this->filled($key)) { + return true; + } + } + + return false; + } + + /** + * Apply the callback if the validated inputs contains a non-empty value for the given input item key. + * + * @param string $key + * @param callable $callback + * @param callable|null $default + * @return $this|mixed + */ + public function whenFilled($key, callable $callback, callable $default = null) + { + if ($this->filled($key)) { + return $callback(data_get($this->all(), $key)) ?: $this; + } + + if ($default) { + return $default(); + } + + return $this; + } + + /** + * Apply the callback if the validated inputs is missing the given input item key. + * + * @param string $key + * @param callable $callback + * @param callable|null $default + * @return $this|mixed + */ + public function whenMissing($key, callable $callback, callable $default = null) + { + if ($this->missing($key)) { + return $callback(data_get($this->all(), $key)) ?: $this; + } + + if ($default) { + return $default(); + } + + return $this; + } + + /** + * Determine if the given input key is an empty string for "filled". + * + * @param string $key + * @return bool + */ + protected function isEmptyString($key) + { + $value = $this->input($key); + + return ! is_bool($value) && ! is_array($value) && trim((string) $value) === ''; + } + + /** + * Get the keys for all of the input. + * + * @return array + */ + public function keys() + { + return array_keys($this->input()); + } + + /** + * Retrieve an input item from the validated inputs. + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function input($key = null, $default = null) + { + return data_get( + $this->all(), $key, $default + ); + } + + /** + * Retrieve input from the validated inputs as a Stringable instance. + * + * @param string $key + * @param mixed $default + * @return \Illuminate\Support\Stringable + */ + public function str($key, $default = null) + { + return $this->string($key, $default); + } + + /** + * Retrieve input from the validated inputs as a Stringable instance. + * + * @param string $key + * @param mixed $default + * @return \Illuminate\Support\Stringable + */ + public function string($key, $default = null) + { + return str($this->input($key, $default)); + } + + /** + * Retrieve input as a boolean value. + * + * Returns true when value is "1", "true", "on", and "yes". Otherwise, returns false. + * + * @param string|null $key + * @param bool $default + * @return bool + */ + public function boolean($key = null, $default = false) + { + return filter_var($this->input($key, $default), FILTER_VALIDATE_BOOLEAN); + } + + /** + * Retrieve input as an integer value. + * + * @param string $key + * @param int $default + * @return int + */ + public function integer($key, $default = 0) + { + return intval($this->input($key, $default)); + } + + /** + * Retrieve input as a float value. + * + * @param string $key + * @param float $default + * @return float + */ + public function float($key, $default = 0.0) + { + return floatval($this->input($key, $default)); + } + + /** + * Retrieve input from the validated inputs as a Carbon instance. + * + * @param string $key + * @param string|null $format + * @param string|null $tz + * @return \Illuminate\Support\Carbon|null + * + * @throws \Carbon\Exceptions\InvalidFormatException + */ + public function date($key, $format = null, $tz = null) + { + if ($this->isNotFilled($key)) { + return null; + } + + if (is_null($format)) { + return Date::parse($this->input($key), $tz); + } + + return Date::createFromFormat($format, $this->input($key), $tz); + } + + /** + * Retrieve input from the validated inputs as an enum. + * + * @template TEnum + * + * @param string $key + * @param class-string $enumClass + * @return TEnum|null + */ + public function enum($key, $enumClass) + { + if ($this->isNotFilled($key) || + ! enum_exists($enumClass) || + ! method_exists($enumClass, 'tryFrom')) { + return null; + } + + return $enumClass::tryFrom($this->input($key)); + } + + /** + * Dump the validated inputs items and end the script. + * + * @param mixed ...$keys + * @return never + */ + public function dd(...$keys) + { + $this->dump(...$keys); + + exit(1); + } + + /** + * Dump the items. + * + * @param mixed $keys + * @return $this + */ + public function dump($keys = []) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + VarDumper::dump(count($keys) > 0 ? $this->only($keys) : $this->all()); + + return $this; + } } diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index dc9b405c0565..6a583f13b651 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -26,6 +26,7 @@ use PHPUnit\Framework\ExpectationFailedException; use ReflectionProperty; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; use Symfony\Component\HttpFoundation\StreamedResponse; /** @@ -309,7 +310,7 @@ public function assertHeaderMissing($headerName) public function assertLocation($uri) { PHPUnit::assertEquals( - app('url')->to($uri), app('url')->to($this->headers->get('Location')) + app('url')->to($uri), app('url')->to($this->headers->get('Location', '')) ); return $this; @@ -323,7 +324,7 @@ public function assertLocation($uri) */ public function assertDownload($filename = null) { - $contentDisposition = explode(';', $this->headers->get('content-disposition')); + $contentDisposition = explode(';', $this->headers->get('content-disposition', '')); if (trim($contentDisposition[0]) !== 'attachment') { PHPUnit::fail( @@ -531,6 +532,17 @@ public function assertStreamedContent($value) return $this; } + /** + * Assert that the given array matches the streamed JSON response content. + * + * @param array $value + * @return $this + */ + public function assertStreamedJsonContent($value) + { + return $this->assertStreamedContent(json_encode($value, JSON_THROW_ON_ERROR)); + } + /** * Assert that the given string or array of strings are contained within the response. * @@ -1564,7 +1576,8 @@ public function streamedContent() return $this->streamedContent; } - if (! $this->baseResponse instanceof StreamedResponse) { + if (! $this->baseResponse instanceof StreamedResponse + && ! $this->baseResponse instanceof StreamedJsonResponse) { PHPUnit::fail('The response is not a streamed response.'); } @@ -1638,7 +1651,7 @@ public function transformNotSuccessfulException($exception) */ protected function appendExceptionToException($exceptionToAppend, $exception) { - $exceptionMessage = $exceptionToAppend->getMessage(); + $exceptionMessage = is_string($exceptionToAppend) ? $exceptionToAppend : $exceptionToAppend->getMessage(); $exceptionToAppend = (string) $exceptionToAppend; diff --git a/src/Illuminate/Translation/PotentiallyTranslatedString.php b/src/Illuminate/Translation/PotentiallyTranslatedString.php index f46db3522429..efcccca28331 100644 --- a/src/Illuminate/Translation/PotentiallyTranslatedString.php +++ b/src/Illuminate/Translation/PotentiallyTranslatedString.php @@ -57,7 +57,7 @@ public function translate($replace = [], $locale = null) /** * Translates the string based on a count. * - * @param \Countable|int|array $number + * @param \Countable|int|float|array $number * @param array $replace * @param string|null $locale * @return $this diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index f9f8b49cb11c..7b9ab9a56d6c 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -160,9 +160,9 @@ public function get($key, array $replace = [], $locale = null, $fallback = true) // the translator was instantiated. Then, we can load the lines and return. $locales = $fallback ? $this->localeArray($locale) : [$locale]; - foreach ($locales as $locale) { + foreach ($locales as $languageLineLocale) { if (! is_null($line = $this->getLine( - $namespace, $group, $locale, $item, $replace + $namespace, $group, $languageLineLocale, $item, $replace ))) { return $line; } @@ -183,7 +183,7 @@ public function get($key, array $replace = [], $locale = null, $fallback = true) * Get a translation according to an integer value. * * @param string $key - * @param \Countable|int|array $number + * @param \Countable|int|float|array $number * @param array $replace * @param string|null $locale * @return string diff --git a/src/Illuminate/Validation/Concerns/FormatsMessages.php b/src/Illuminate/Validation/Concerns/FormatsMessages.php index 4d4042a4e10f..2d68828d1b3a 100644 --- a/src/Illuminate/Validation/Concerns/FormatsMessages.php +++ b/src/Illuminate/Validation/Concerns/FormatsMessages.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\UploadedFile; trait FormatsMessages @@ -218,15 +219,13 @@ protected function getAttributeType($attribute) // We assume that the attributes present in the file array are files so that // means that if the attribute does not have a numeric rule and the files // list doesn't have it we'll just consider it a string by elimination. - if ($this->hasRule($attribute, $this->numericRules)) { - return 'numeric'; - } elseif ($this->hasRule($attribute, ['Array'])) { - return 'array'; - } elseif ($this->getValue($attribute) instanceof UploadedFile) { - return 'file'; - } - - return 'string'; + return match (true) { + $this->hasRule($attribute, $this->numericRules) => 'numeric', + $this->hasRule($attribute, ['Array']) => 'array', + $this->getValue($attribute) instanceof UploadedFile, + $this->getValue($attribute) instanceof File => 'file', + default => 'string', + }; } /** diff --git a/src/Illuminate/Validation/ConditionalRules.php b/src/Illuminate/Validation/ConditionalRules.php index 02f8e21fb7fa..fa6022209672 100644 --- a/src/Illuminate/Validation/ConditionalRules.php +++ b/src/Illuminate/Validation/ConditionalRules.php @@ -16,14 +16,14 @@ class ConditionalRules /** * The rules to be added to the attribute. * - * @var array|string|\Closure + * @var \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string */ protected $rules; /** * The rules to be added to the attribute if the condition fails. * - * @var array|string|\Closure + * @var \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string */ protected $defaultRules; @@ -31,8 +31,8 @@ class ConditionalRules * Create a new conditional rules instance. * * @param callable|bool $condition - * @param array|string|\Closure $rules - * @param array|string|\Closure $defaultRules + * @param \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string $rules + * @param \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string $defaultRules * @return void */ public function __construct($condition, $rules, $defaultRules = []) diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 1f8286cc375f..349e1453c8f6 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -37,8 +37,8 @@ public static function can($ability, ...$arguments) * Apply the given rules if the given condition is truthy. * * @param callable|bool $condition - * @param array|string|\Closure $rules - * @param array|string|\Closure $defaultRules + * @param \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string $rules + * @param \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string $defaultRules * @return \Illuminate\Validation\ConditionalRules */ public static function when($condition, $rules, $defaultRules = []) @@ -50,13 +50,13 @@ public static function when($condition, $rules, $defaultRules = []) * Apply the given rules if the given condition is falsy. * * @param callable|bool $condition - * @param array|string|\Closure $rules - * @param array|string|\Closure $defaultRules + * @param \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string $rules + * @param \Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\InvokableRule|\Illuminate\Contracts\Validation\Rule|\Closure|array|string $defaultRules * @return \Illuminate\Validation\ConditionalRules */ public static function unless($condition, $rules, $defaultRules = []) { - return new ConditionalRules(! $condition, $rules, $defaultRules); + return new ConditionalRules($condition, $defaultRules, $rules); } /** diff --git a/src/Illuminate/Validation/Rules/Enum.php b/src/Illuminate/Validation/Rules/Enum.php index d66a16d126bc..62114626eca7 100644 --- a/src/Illuminate/Validation/Rules/Enum.php +++ b/src/Illuminate/Validation/Rules/Enum.php @@ -4,10 +4,14 @@ use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidatorAwareRule; +use Illuminate\Support\Arr; +use Illuminate\Support\Traits\Conditionable; use TypeError; class Enum implements Rule, ValidatorAwareRule { + use Conditionable; + /** * The type of the enum. * @@ -22,6 +26,20 @@ class Enum implements Rule, ValidatorAwareRule */ protected $validator; + /** + * The cases that should be considered valid. + * + * @var array + */ + protected $only = []; + + /** + * The cases that should be considered invalid. + * + * @var array + */ + protected $except = []; + /** * Create a new rule instance. * @@ -43,7 +61,7 @@ public function __construct($type) public function passes($attribute, $value) { if ($value instanceof $this->type) { - return true; + return $this->isDesirable($value); } if (is_null($value) || ! enum_exists($this->type) || ! method_exists($this->type, 'tryFrom')) { @@ -51,12 +69,55 @@ public function passes($attribute, $value) } try { - return ! is_null($this->type::tryFrom($value)); + $value = $this->type::tryFrom($value); + + return ! is_null($value) && $this->isDesirable($value); } catch (TypeError) { return false; } } + /** + * Specify the cases that should be considered valid. + * + * @param \UnitEnum[]|\UnitEnum $values + * @return $this + */ + public function only($values) + { + $this->only = Arr::wrap($values); + + return $this; + } + + /** + * Specify the cases that should be considered invalid. + * + * @param \UnitEnum[]|\UnitEnum $values + * @return $this + */ + public function except($values) + { + $this->except = Arr::wrap($values); + + return $this; + } + + /** + * Determine if the given case is a valid case based on the only / except values. + * + * @param mixed $value + * @return bool + */ + protected function isDesirable($value) + { + return match (true) { + ! empty($this->only) => in_array(needle: $value, haystack: $this->only, strict: true), + ! empty($this->except) => ! in_array(needle: $value, haystack: $this->except, strict: true), + default => true, + }; + } + /** * Get the validation error message. * diff --git a/src/Illuminate/Validation/Rules/File.php b/src/Illuminate/Validation/Rules/File.php index fb9a0603c2d6..e93e1759df43 100644 --- a/src/Illuminate/Validation/Rules/File.php +++ b/src/Illuminate/Validation/Rules/File.php @@ -14,8 +14,7 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule { - use Conditionable; - use Macroable; + use Conditionable, Macroable; /** * The MIME types that the given file should match. This array may also contain file extensions. diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 93e06dc5d57a..3fb66e9ee2bd 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -37,6 +37,13 @@ class Password implements Rule, DataAwareRule, ValidatorAwareRule */ protected $min = 8; + /** + * The maximum size of the password. + * + * @var int + */ + protected $max; + /** * If the password requires at least one uppercase and one lowercase letter. * @@ -193,7 +200,7 @@ public function setData($data) } /** - * Sets the minimum size of the password. + * Set the minimum size of the password. * * @param int $size * @return $this @@ -203,6 +210,19 @@ public static function min($size) return new static($size); } + /** + * Set the maximum size of the password. + * + * @param int $size + * @return $this + */ + public function max($size) + { + $this->max = $size; + + return $this; + } + /** * Ensures the password has not been compromised in data leaks. * @@ -292,7 +312,12 @@ public function passes($attribute, $value) $validator = Validator::make( $this->data, - [$attribute => array_merge(['string', 'min:'.$this->min], $this->customRules)], + [$attribute => [ + 'string', + 'min:'.$this->min, + ...($this->max ? ['max:'.$this->max] : []), + ...$this->customRules, + ]], $this->validator->customMessages, $this->validator->customAttributes )->after(function ($validator) use ($attribute, $value) { diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 35d18149af79..ed5028e322e4 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -584,14 +584,15 @@ public function validated() $missingValue = new stdClass; foreach ($this->getRules() as $key => $rules) { + $value = data_get($this->getData(), $key, $missingValue); + if ($this->excludeUnvalidatedArrayKeys && in_array('array', $rules) && + $value !== null && ! empty(preg_grep('/^'.preg_quote($key, '/').'\.+/', array_keys($this->getRules())))) { continue; } - $value = data_get($this->getData(), $key, $missingValue); - if ($value !== $missingValue) { Arr::set($results, $key, $value); } diff --git a/src/Illuminate/Validation/composer.json b/src/Illuminate/Validation/composer.json index 611f05092f38..73670e87f98e 100755 --- a/src/Illuminate/Validation/composer.json +++ b/src/Illuminate/Validation/composer.json @@ -17,7 +17,7 @@ "php": "^8.1", "ext-filter": "*", "ext-mbstring": "*", - "brick/math": "^0.9.3|^0.10.2|^0.11", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "egulias/email-validator": "^3.2.5|^4.0", "illuminate/collections": "^10.0", "illuminate/container": "^10.0", diff --git a/src/Illuminate/View/ComponentSlot.php b/src/Illuminate/View/ComponentSlot.php index 85665ad64575..dc48dbc88798 100644 --- a/src/Illuminate/View/ComponentSlot.php +++ b/src/Illuminate/View/ComponentSlot.php @@ -3,6 +3,7 @@ namespace Illuminate\View; use Illuminate\Contracts\Support\Htmlable; +use InvalidArgumentException; class ComponentSlot implements Htmlable { @@ -77,6 +78,25 @@ public function isNotEmpty() return ! $this->isEmpty(); } + /** + * Determine if the slot has non-comment content. + * + * @param callable|string|null $callable + * @return bool + */ + public function hasActualContent(callable|string|null $callable = null) + { + if (is_string($callable) && ! function_exists($callable)) { + throw new InvalidArgumentException('Callable does not exist.'); + } + + return filter_var( + $this->contents, + FILTER_CALLBACK, + ['options' => $callable ?? fn ($input) => trim(preg_replace("//", '', $input))] + ) !== ''; + } + /** * Get the slot's HTML string. * diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index c40c3b9fc5bb..41cd8b93c9aa 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -2,6 +2,7 @@ namespace Illuminate\View; +use Illuminate\Container\Container; use Illuminate\Support\ServiceProvider; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Engines\CompilerEngine; @@ -135,7 +136,7 @@ public function registerEngineResolver() public function registerFileEngine($resolver) { $resolver->register('file', function () { - return new FileEngine($this->app['files']); + return new FileEngine(Container::getInstance()->make('files')); }); } @@ -148,7 +149,7 @@ public function registerFileEngine($resolver) public function registerPhpEngine($resolver) { $resolver->register('php', function () { - return new PhpEngine($this->app['files']); + return new PhpEngine(Container::getInstance()->make('files')); }); } @@ -161,9 +162,14 @@ public function registerPhpEngine($resolver) public function registerBladeEngine($resolver) { $resolver->register('blade', function () { - $compiler = new CompilerEngine($this->app['blade.compiler'], $this->app['files']); + $app = Container::getInstance(); - $this->app->terminating(static function () use ($compiler) { + $compiler = new CompilerEngine( + $app->make('blade.compiler'), + $app->make('files'), + ); + + $app->terminating(static function () use ($compiler) { $compiler->forgetCompiledOrNotExpired(); }); diff --git a/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php b/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php index 2b888f59f07e..9ef72a9fd726 100644 --- a/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php +++ b/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php @@ -6,6 +6,7 @@ use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User; +use Mockery as m; use PHPUnit\Framework\TestCase; class AuthListenersSendEmailVerificationNotificationHandleFunctionTest extends TestCase @@ -29,8 +30,8 @@ public function testWillExecuted() */ public function testUserIsNotInstanceOfMustVerifyEmail() { - $user = $this->getMockBuilder(User::class)->getMock(); - $user->expects($this->never())->method('sendEmailVerificationNotification'); + $user = m::mock(User::class); + $user->shouldNotReceive('sendEmailVerificationNotification'); $listener = new SendEmailVerificationNotification; diff --git a/tests/Bus/BusPendingBatchTest.php b/tests/Bus/BusPendingBatchTest.php index 6a6e9186e686..d5b9bd8d9ea7 100644 --- a/tests/Bus/BusPendingBatchTest.php +++ b/tests/Bus/BusPendingBatchTest.php @@ -37,7 +37,9 @@ public function test_pending_batch_may_be_configured_and_dispatched() $pendingBatch = new PendingBatch($container, new Collection([$job])); - $pendingBatch = $pendingBatch->progress(function () { + $pendingBatch = $pendingBatch->before(function () { + // + })->progress(function () { // })->then(function () { // @@ -47,6 +49,7 @@ public function test_pending_batch_may_be_configured_and_dispatched() $this->assertSame('test-connection', $pendingBatch->connection()); $this->assertSame('test-queue', $pendingBatch->queue()); + $this->assertCount(1, $pendingBatch->beforeCallbacks()); $this->assertCount(1, $pendingBatch->progressCallbacks()); $this->assertCount(1, $pendingBatch->thenCallbacks()); $this->assertCount(1, $pendingBatch->catchCallbacks()); @@ -186,4 +189,37 @@ public function test_batch_is_not_dispatched_when_dispatchunless_is_true() $this->assertNull($result); } + + public function test_batch_before_event_is_called() + { + $container = new Container; + + $eventDispatcher = m::mock(Dispatcher::class); + $eventDispatcher->shouldReceive('dispatch')->once(); + + $container->instance(Dispatcher::class, $eventDispatcher); + + $job = new class + { + use Batchable; + }; + + $beforeCalled = false; + + $pendingBatch = new PendingBatch($container, new Collection([$job])); + + $pendingBatch = $pendingBatch->before(function () use (&$beforeCalled) { + $beforeCalled = true; + })->onConnection('test-connection')->onQueue('test-queue'); + + $repository = m::mock(BatchRepository::class); + $repository->shouldReceive('store')->once()->with($pendingBatch)->andReturn($batch = m::mock(stdClass::class)); + $batch->shouldReceive('add')->once()->with(m::type(Collection::class))->andReturn($batch = m::mock(Batch::class)); + + $container->instance(BatchRepository::class, $repository); + + $pendingBatch->dispatch(); + + $this->assertTrue($beforeCalled); + } } diff --git a/tests/Cache/CacheRateLimiterTest.php b/tests/Cache/CacheRateLimiterTest.php index 660179256c84..0805f92af092 100644 --- a/tests/Cache/CacheRateLimiterTest.php +++ b/tests/Cache/CacheRateLimiterTest.php @@ -30,18 +30,29 @@ public function testHitProperlyIncrementsAttemptCount() $cache = m::mock(Cache::class); $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); - $cache->shouldReceive('increment')->once()->with('key')->andReturn(1); + $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $rateLimiter = new RateLimiter($cache); $rateLimiter->hit('key', 1); } + public function testIncrementProperlyIncrementsAttemptCount() + { + $cache = m::mock(Cache::class); + $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); + $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(true); + $cache->shouldReceive('increment')->once()->with('key', 5)->andReturn(5); + $rateLimiter = new RateLimiter($cache); + + $rateLimiter->increment('key', 1, 5); + } + public function testHitHasNoMemoryLeak() { $cache = m::mock(Cache::class); $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1)->andReturn(true); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturn(false); - $cache->shouldReceive('increment')->once()->with('key')->andReturn(1); + $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $cache->shouldReceive('put')->once()->with('key', 1, 1); $rateLimiter = new RateLimiter($cache); @@ -83,7 +94,7 @@ public function testAttemptsCallbackReturnsTrue() $cache->shouldReceive('get')->once()->with('key', 0)->andReturn(0); $cache->shouldReceive('add')->once()->with('key:timer', m::type('int'), 1); $cache->shouldReceive('add')->once()->with('key', 0, 1)->andReturns(1); - $cache->shouldReceive('increment')->once()->with('key')->andReturn(1); + $cache->shouldReceive('increment')->once()->with('key', 1)->andReturn(1); $executed = false; @@ -101,7 +112,7 @@ public function testAttemptsCallbackReturnsCallbackReturn() $cache->shouldReceive('get')->times(6)->with('key', 0)->andReturn(0); $cache->shouldReceive('add')->times(6)->with('key:timer', m::type('int'), 1); $cache->shouldReceive('add')->times(6)->with('key', 0, 1)->andReturns(1); - $cache->shouldReceive('increment')->times(6)->with('key')->andReturn(1); + $cache->shouldReceive('increment')->times(6)->with('key', 1)->andReturn(1); $rateLimiter = new RateLimiter($cache); diff --git a/tests/Database/DatabaseConcernsHasAttributesTest.php b/tests/Database/DatabaseConcernsHasAttributesTest.php index 2e2159c4c102..f4e31d4122a4 100644 --- a/tests/Database/DatabaseConcernsHasAttributesTest.php +++ b/tests/Database/DatabaseConcernsHasAttributesTest.php @@ -21,6 +21,14 @@ public function testWithConstructorArguments() $attributes = $instance->getMutatedAttributes(); $this->assertEquals(['some_attribute'], $attributes); } + + public function testCastingEmptyStringToArrayDoesNotError() + { + $instance = new HasAttributesWithArrayCast(); + $this->assertEquals(['foo' => null], $instance->attributesToArray()); + + $this->assertTrue(json_last_error() === JSON_ERROR_NONE); + } } class HasAttributesWithoutConstructor @@ -40,3 +48,23 @@ public function __construct($someValue) { } } + +class HasAttributesWithArrayCast +{ + use HasAttributes; + + public function getArrayableAttributes(): array + { + return ['foo' => '']; + } + + public function getCasts(): array + { + return ['foo' => 'array']; + } + + public function usesTimestamps(): bool + { + return false; + } +} diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index 8709e339c226..1b6211386a04 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -484,6 +484,18 @@ public function testBeforeExecutingHooksCanBeRegistered() $connection->select('foo bar', ['baz']); } + public function testBeforeStartingTransactionHooksCanBeRegistered() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The callback was fired'); + + $connection = $this->getMockConnection(); + $connection->beforeStartingTransaction(function () { + throw new Exception('The callback was fired'); + }); + $connection->beginTransaction(); + } + public function testPretendOnlyLogsQueries() { $connection = $this->getMockConnection(); diff --git a/tests/Database/DatabaseConnectorTest.php b/tests/Database/DatabaseConnectorTest.php index 79ee6d0606c4..7990ea285858 100755 --- a/tests/Database/DatabaseConnectorTest.php +++ b/tests/Database/DatabaseConnectorTest.php @@ -75,15 +75,15 @@ public function testMySqlConnectCallsCreateConnectionWithIsolationLevel() public function testPostgresConnectCallsCreateConnectionWithProperArguments() { - $dsn = 'pgsql:host=foo;dbname=\'bar\';port=111'; + $dsn = 'pgsql:host=foo;dbname=\'bar\';port=111;client_encoding=\'utf8\''; $config = ['host' => 'foo', 'database' => 'bar', 'port' => 111, 'charset' => 'utf8']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); $statement = m::mock(PDOStatement::class); - $connection->shouldReceive('prepare')->once()->with('set names \'utf8\'')->andReturn($statement); - $statement->shouldReceive('execute')->once(); + $connection->shouldReceive('prepare')->zeroOrMoreTimes()->andReturn($statement); + $statement->shouldReceive('execute')->zeroOrMoreTimes(); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -97,16 +97,15 @@ public function testPostgresConnectCallsCreateConnectionWithProperArguments() */ public function testPostgresSearchPathIsSet($searchPath, $expectedSql) { - $dsn = 'pgsql:host=foo;dbname=\'bar\''; + $dsn = 'pgsql:host=foo;dbname=\'bar\';client_encoding=\'utf8\''; $config = ['host' => 'foo', 'database' => 'bar', 'search_path' => $searchPath, 'charset' => 'utf8']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); $statement = m::mock(PDOStatement::class); - $connection->shouldReceive('prepare')->once()->with('set names \'utf8\'')->andReturn($statement); $connection->shouldReceive('prepare')->once()->with($expectedSql)->andReturn($statement); - $statement->shouldReceive('execute')->twice(); + $statement->shouldReceive('execute')->once(); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -184,16 +183,15 @@ public static function provideSearchPaths() public function testPostgresSearchPathFallbackToConfigKeySchema() { - $dsn = 'pgsql:host=foo;dbname=\'bar\''; + $dsn = 'pgsql:host=foo;dbname=\'bar\';client_encoding=\'utf8\''; $config = ['host' => 'foo', 'database' => 'bar', 'schema' => ['public', '"user"'], 'charset' => 'utf8']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); $statement = m::mock(PDOStatement::class); - $connection->shouldReceive('prepare')->once()->with('set names \'utf8\'')->andReturn($statement); $connection->shouldReceive('prepare')->once()->with('set search_path to "public", "user"')->andReturn($statement); - $statement->shouldReceive('execute')->twice(); + $statement->shouldReceive('execute')->once(); $result = $connector->connect($config); $this->assertSame($result, $connection); @@ -201,16 +199,15 @@ public function testPostgresSearchPathFallbackToConfigKeySchema() public function testPostgresApplicationNameIsSet() { - $dsn = 'pgsql:host=foo;dbname=\'bar\''; + $dsn = 'pgsql:host=foo;dbname=\'bar\';client_encoding=\'utf8\';application_name=\'Laravel App\''; $config = ['host' => 'foo', 'database' => 'bar', 'charset' => 'utf8', 'application_name' => 'Laravel App']; $connector = $this->getMockBuilder(PostgresConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); $connection = m::mock(stdClass::class); $connector->expects($this->once())->method('getOptions')->with($this->equalTo($config))->willReturn(['options']); $connector->expects($this->once())->method('createConnection')->with($this->equalTo($dsn), $this->equalTo($config), $this->equalTo(['options']))->willReturn($connection); $statement = m::mock(PDOStatement::class); - $connection->shouldReceive('prepare')->once()->with('set names \'utf8\'')->andReturn($statement); - $connection->shouldReceive('prepare')->once()->with('set application_name to \'Laravel App\'')->andReturn($statement); - $statement->shouldReceive('execute')->twice(); + $connection->shouldReceive('prepare')->zeroOrMoreTimes()->andReturn($statement); + $statement->shouldReceive('execute')->zeroOrMoreTimes(); $result = $connector->connect($config); $this->assertSame($result, $connection); diff --git a/tests/Database/DatabaseEloquentBelongsToManyAggregateTest.php b/tests/Database/DatabaseEloquentBelongsToManyAggregateTest.php index 04db5bba5e7d..7847787984a3 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyAggregateTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyAggregateTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Model as Eloquent; +use Illuminate\Database\Query\Expression; use PHPUnit\Framework\TestCase; class DatabaseEloquentBelongsToManyAggregateTest extends TestCase @@ -45,6 +46,17 @@ public function testWithSumSameTable() $this->assertEquals(1200, $order->total_allocated); } + public function testWithSumExpression() + { + $this->seedData(); + + $order = BelongsToManyAggregateTestTestTransaction::query() + ->withSum('allocatedTo as total_allocated', new Expression('allocations.amount * 2')) + ->first(); + + $this->assertEquals(2400, $order->total_allocated); + } + /** * Setup the database schema. * diff --git a/tests/Database/DatabaseEloquentBelongsToManyChunkByIdTest.php b/tests/Database/DatabaseEloquentBelongsToManyChunkByIdTest.php index f35c2f9a3ce9..0b9d8d51d93c 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyChunkByIdTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyChunkByIdTest.php @@ -37,13 +37,14 @@ public function createSchema() }); $this->schema()->create('articles', function ($table) { - $table->increments('aid'); + $table->increments('id'); $table->string('title'); }); $this->schema()->create('article_user', function ($table) { + $table->increments('id'); $table->integer('article_id')->unsigned(); - $table->foreign('article_id')->references('aid')->on('articles'); + $table->foreign('article_id')->references('id')->on('articles'); $table->integer('user_id')->unsigned(); $table->foreign('user_id')->references('id')->on('users'); }); @@ -58,7 +59,22 @@ public function testBelongsToChunkById() $user->articles()->chunkById(1, function (Collection $collection) use (&$i) { $i++; - $this->assertEquals($i, $collection->first()->aid); + $this->assertEquals($i, $collection->first()->id); + }); + + $this->assertSame(3, $i); + } + + public function testBelongsToChunkByIdDesc() + { + $this->seedData(); + + $user = BelongsToManyChunkByIdTestTestUser::query()->first(); + $i = 0; + + $user->articles()->chunkByIdDesc(1, function (Collection $collection) use (&$i) { + $this->assertEquals(3 - $i, $collection->first()->id); + $i++; }); $this->assertSame(3, $i); @@ -83,9 +99,9 @@ protected function seedData() { $user = BelongsToManyChunkByIdTestTestUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); BelongsToManyChunkByIdTestTestArticle::query()->insert([ - ['aid' => 1, 'title' => 'Another title'], - ['aid' => 2, 'title' => 'Another title'], - ['aid' => 3, 'title' => 'Another title'], + ['id' => 1, 'title' => 'Another title'], + ['id' => 2, 'title' => 'Another title'], + ['id' => 3, 'title' => 'Another title'], ]); $user->articles()->sync([3, 1, 2]); @@ -126,10 +142,9 @@ public function articles() class BelongsToManyChunkByIdTestTestArticle extends Eloquent { - protected $primaryKey = 'aid'; protected $table = 'articles'; protected $keyType = 'string'; public $incrementing = false; public $timestamps = false; - protected $fillable = ['aid', 'title']; + protected $fillable = ['id', 'title']; } diff --git a/tests/Database/DatabaseEloquentBelongsToManyEachByIdTest.php b/tests/Database/DatabaseEloquentBelongsToManyEachByIdTest.php new file mode 100644 index 000000000000..0a2fe1e97a06 --- /dev/null +++ b/tests/Database/DatabaseEloquentBelongsToManyEachByIdTest.php @@ -0,0 +1,134 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + + $this->createSchema(); + } + + /** + * Setup the database schema. + * + * @return void + */ + public function createSchema() + { + $this->schema()->create('users', function ($table) { + $table->increments('id'); + $table->string('email')->unique(); + }); + + $this->schema()->create('articles', function ($table) { + $table->increments('id'); + $table->string('title'); + }); + + $this->schema()->create('article_user', function ($table) { + $table->increments('id'); + $table->integer('article_id')->unsigned(); + $table->foreign('article_id')->references('id')->on('articles'); + $table->integer('user_id')->unsigned(); + $table->foreign('user_id')->references('id')->on('users'); + }); + } + + public function testBelongsToEachById() + { + $this->seedData(); + + $user = BelongsToManyEachByIdTestTestUser::query()->first(); + $i = 0; + + $user->articles()->eachById(function (BelongsToManyEachByIdTestTestArticle $model) use (&$i) { + $i++; + $this->assertEquals($i, $model->id); + }); + + $this->assertSame(3, $i); + } + + /** + * Tear down the database schema. + * + * @return void + */ + protected function tearDown(): void + { + $this->schema()->drop('users'); + $this->schema()->drop('articles'); + $this->schema()->drop('article_user'); + } + + /** + * Helpers... + */ + protected function seedData() + { + $user = BelongsToManyEachByIdTestTestUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); + BelongsToManyEachByIdTestTestArticle::query()->insert([ + ['id' => 1, 'title' => 'Another title'], + ['id' => 2, 'title' => 'Another title'], + ['id' => 3, 'title' => 'Another title'], + ]); + + $user->articles()->sync([3, 1, 2]); + } + + /** + * Get a database connection instance. + * + * @return \Illuminate\Database\ConnectionInterface + */ + protected function connection() + { + return Eloquent::getConnectionResolver()->connection(); + } + + /** + * Get a schema builder instance. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function schema() + { + return $this->connection()->getSchemaBuilder(); + } +} + +class BelongsToManyEachByIdTestTestUser extends Eloquent +{ + protected $table = 'users'; + protected $fillable = ['id', 'email']; + public $timestamps = false; + + public function articles() + { + return $this->belongsToMany(BelongsToManyEachByIdTestTestArticle::class, 'article_user', 'user_id', 'article_id'); + } +} + +class BelongsToManyEachByIdTestTestArticle extends Eloquent +{ + protected $table = 'articles'; + protected $keyType = 'string'; + public $incrementing = false; + public $timestamps = false; + protected $fillable = ['id', 'title']; +} diff --git a/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php b/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php new file mode 100644 index 000000000000..32d617e36fb2 --- /dev/null +++ b/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php @@ -0,0 +1,65 @@ +makePartial(); + $related->shouldReceive('getUpdatedAtColumn')->never(); + $related->shouldReceive('freshTimestampString')->never(); + + $this->assertFalse($related::isIgnoringTouch()); + + Model::withoutTouching(function () use ($related) { + $this->assertTrue($related::isIgnoringTouch()); + + $builder = m::mock(Builder::class); + $builder->shouldReceive('join'); + $parent = m::mock(User::class); + + $parent->shouldReceive('getAttribute')->with('id')->andReturn(1); + $builder->shouldReceive('getModel')->andReturn($related); + $builder->shouldReceive('where'); + $relation = new BelongsToMany($builder, $parent, 'article_users', 'user_id', 'article_id', 'id', 'id'); + $builder->shouldReceive('update')->never(); + + $relation->touch(); + }); + + $this->assertFalse($related::isIgnoringTouch()); + } +} + +class User extends Model +{ + protected $table = 'users'; + protected $fillable = ['id', 'email']; + + public function articles(): BelongsToMany + { + return $this->belongsToMany(Article::class, 'article_user', 'user_id', 'article_id'); + } +} + +class Article extends Model +{ + protected $table = 'articles'; + protected $fillable = ['id', 'title']; + protected $touches = ['user']; + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'article_user', 'article_id', 'user_id'); + } +} diff --git a/tests/Database/DatabaseEloquentBelongsToTest.php b/tests/Database/DatabaseEloquentBelongsToTest.php index 0c392e704412..1ace437a4346 100755 --- a/tests/Database/DatabaseEloquentBelongsToTest.php +++ b/tests/Database/DatabaseEloquentBelongsToTest.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Tests\Database\Fixtures\Enums\Bar; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -85,6 +86,16 @@ public function testIdsInEagerConstraintsCanBeZero() $relation->addEagerConstraints($models); } + public function testIdsInEagerConstraintsCanBeBackedEnum() + { + $relation = $this->getRelation(); + $relation->getRelated()->shouldReceive('getKeyName')->andReturn('id'); + $relation->getRelated()->shouldReceive('getKeyType')->andReturn('int'); + $relation->getQuery()->shouldReceive('whereIntegerInRaw')->once()->with('relation.id', [5, 'foreign.value']); + $models = [new EloquentBelongsToModelStub, new EloquentBelongsToModelStubWithBackedEnumCast]; + $relation->addEagerConstraints($models); + } + public function testRelationIsProperlyInitialized() { $relation = $this->getRelation(); @@ -119,6 +130,15 @@ public function __toString() } }; + $result4 = new class extends Model + { + protected $casts = [ + 'id' => Bar::class, + ]; + + protected $attributes = ['id' => 5]; + }; + $model1 = new EloquentBelongsToModelStub; $model1->foreign_key = 1; $model2 = new EloquentBelongsToModelStub; @@ -131,11 +151,18 @@ public function __toString() return '3'; } }; - $models = $relation->match([$model1, $model2, $model3], new Collection([$result1, $result2, $result3]), 'foo'); + $model4 = new EloquentBelongsToModelStub; + $model4->foreign_key = 5; + $models = $relation->match( + [$model1, $model2, $model3, $model4], + new Collection([$result1, $result2, $result3, $result4]), + 'foo' + ); $this->assertEquals(1, $models[0]->foo->getAttribute('id')); $this->assertEquals(2, $models[1]->foo->getAttribute('id')); $this->assertSame('3', (string) $models[2]->foo->getAttribute('id')); + $this->assertEquals(5, $models[3]->foo->getAttribute('id')->value); } public function testAssociateMethodSetsForeignKeyOnModel() @@ -403,3 +430,14 @@ class MissingEloquentBelongsToModelStub extends Model { public $foreign_key; } + +class EloquentBelongsToModelStubWithBackedEnumCast extends Model +{ + protected $casts = [ + 'foreign_key' => Bar::class, + ]; + + public $attributes = [ + 'foreign_key' => 5, + ]; +} diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 31354f2c1ab0..a2b13bd867cf 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Builder as BaseBuilder; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\Processors\Processor; use Illuminate\Support\Carbon; @@ -918,6 +919,11 @@ public function testQueryPassThru() $this->assertSame('foo', $builder->insertOrIgnore(['bar'])); + $builder = $this->getBuilder(); + $builder->getQuery()->shouldReceive('insertOrIgnoreUsing')->once()->with(['bar'], 'baz')->andReturn('foo'); + + $this->assertSame('foo', $builder->insertOrIgnoreUsing(['bar'], 'baz')); + $builder = $this->getBuilder(); $builder->getQuery()->shouldReceive('insertGetId')->once()->with(['bar'])->andReturn('foo'); @@ -1273,6 +1279,15 @@ public function testWithMin() $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select min("eloquent_builder_test_model_close_related_stubs"."price") from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_min_price" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); } + public function testWithMinExpression() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMin('foo', new Expression('price - discount')); + + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select min(price - discount) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_min_price_discount" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + public function testWithMinOnBelongsToMany() { $model = new EloquentBuilderTestModelParentStub; @@ -1297,6 +1312,42 @@ public function testWithMinOnSelfRelated() $this->assertSame('select "self_related_stubs".*, (select min("self_alias_hash"."created_at") from "self_related_stubs" as "self_alias_hash" where "self_related_stubs"."id" = "self_alias_hash"."parent_id") as "child_foos_min_created_at" from "self_related_stubs"', $sql); } + public function testWithMax() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMax('foo', 'price'); + + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select max("eloquent_builder_test_model_close_related_stubs"."price") from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_max_price" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWithMaxExpression() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMax('foo', new Expression('price - discount')); + + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select max(price - discount) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_max_price_discount" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWithAvg() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withAvg('foo', 'price'); + + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select avg("eloquent_builder_test_model_close_related_stubs"."price") from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_avg_price" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWitAvgExpression() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withAvg('foo', new Expression('price - discount')); + + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select avg(price - discount) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_avg_price_discount" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + public function testWithCountAndConstraintsAndHaving() { $model = new EloquentBuilderTestModelParentStub; diff --git a/tests/Database/DatabaseEloquentGlobalScopesTest.php b/tests/Database/DatabaseEloquentGlobalScopesTest.php index 7ae26071506d..3b5a379d4fdb 100644 --- a/tests/Database/DatabaseEloquentGlobalScopesTest.php +++ b/tests/Database/DatabaseEloquentGlobalScopesTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; @@ -43,6 +44,22 @@ public function testGlobalScopeCanBeRemoved() $this->assertEquals([], $query->getBindings()); } + public function testClassNameGlobalScopeIsApplied() + { + $model = new EloquentClassNameGlobalScopesTestModel; + $query = $model->newQuery(); + $this->assertSame('select * from "table" where "active" = ?', $query->toSql()); + $this->assertEquals([1], $query->getBindings()); + } + + public function testGlobalScopeInAttributeIsApplied() + { + $model = new EloquentGlobalScopeInAttributeTestModel; + $query = $model->newQuery(); + $this->assertSame('select * from "table" where "active" = ?', $query->toSql()); + $this->assertEquals([1], $query->getBindings()); + } + public function testClosureGlobalScopeIsApplied() { $model = new EloquentClosureGlobalScopesTestModel; @@ -51,6 +68,14 @@ public function testClosureGlobalScopeIsApplied() $this->assertEquals([1], $query->getBindings()); } + public function testGlobalScopesCanBeRegisteredViaArray() + { + $model = new EloquentGlobalScopesArrayTestModel; + $query = $model->newQuery(); + $this->assertSame('select * from "table" where "active" = ? order by "name" asc', $query->toSql()); + $this->assertEquals([1], $query->getBindings()); + } + public function testClosureGlobalScopeCanBeRemoved() { $model = new EloquentClosureGlobalScopesTestModel; @@ -190,6 +215,39 @@ public static function boot() } } +class EloquentClassNameGlobalScopesTestModel extends Model +{ + protected $table = 'table'; + + public static function boot() + { + static::addGlobalScope(ActiveScope::class); + + parent::boot(); + } +} + +class EloquentGlobalScopesArrayTestModel extends Model +{ + protected $table = 'table'; + + public static function boot() + { + static::addGlobalScopes([ + 'active_scope' => new ActiveScope, + fn ($query) => $query->orderBy('name'), + ]); + + parent::boot(); + } +} + +#[ScopedBy(ActiveScope::class)] +class EloquentGlobalScopeInAttributeTestModel extends Model +{ + protected $table = 'table'; +} + class ActiveScope implements Scope { public function apply(Builder $builder, Model $model) diff --git a/tests/Database/DatabaseEloquentLocalScopesTest.php b/tests/Database/DatabaseEloquentLocalScopesTest.php index 1d71f6f57661..d34a510f1e5f 100644 --- a/tests/Database/DatabaseEloquentLocalScopesTest.php +++ b/tests/Database/DatabaseEloquentLocalScopesTest.php @@ -61,6 +61,32 @@ public function testLocalScopesCanChained() $this->assertSame('select * from "table" where "active" = ? and "type" = ?', $query->toSql()); $this->assertEquals([true, 'foo'], $query->getBindings()); } + + public function testLocalScopeNestingDoesntDoubleFirstWhereClauseNegation() + { + $model = new EloquentLocalScopesTestModel; + $query = $model + ->newQuery() + ->whereNot('firstWhere', true) + ->orWhere('secondWhere', true) + ->active(); + + $this->assertSame('select * from "table" where (not "firstWhere" = ? or "secondWhere" = ?) and "active" = ?', $query->toSql()); + $this->assertEquals([true, true, true], $query->getBindings()); + } + + public function testLocalScopeNestingGroupsOrNotWhereClause() + { + $model = new EloquentLocalScopesTestModel; + $query = $model + ->newQuery() + ->where('firstWhere', true) + ->orWhereNot('secondWhere', true) + ->active(); + + $this->assertSame('select * from "table" where ("firstWhere" = ? or not "secondWhere" = ?) and "active" = ?', $query->toSql()); + $this->assertEquals([true, true, true], $query->getBindings()); + } } class EloquentLocalScopesTestModel extends Model diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 395629b4f9b1..15b248c5c622 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -15,6 +15,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\ConnectionResolverInterface as Resolver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\ArrayObject; use Illuminate\Database\Eloquent\Casts\AsArrayObject; @@ -1893,6 +1894,26 @@ public function testModelObserversCanBeAttachedToModelsThroughAnArray() EloquentModelStub::flushEventListeners(); } + public function testModelObserversCanBeAttachedToModelsWithStringUsingAttribute() + { + EloquentModelWithObserveAttributeStub::setEventDispatcher($events = m::mock(Dispatcher::class)); + $events->shouldReceive('dispatch'); + $events->shouldReceive('listen')->once()->with('eloquent.creating: Illuminate\Tests\Database\EloquentModelWithObserveAttributeStub', EloquentTestObserverStub::class.'@creating'); + $events->shouldReceive('listen')->once()->with('eloquent.saved: Illuminate\Tests\Database\EloquentModelWithObserveAttributeStub', EloquentTestObserverStub::class.'@saved'); + $events->shouldReceive('forget'); + EloquentModelWithObserveAttributeStub::flushEventListeners(); + } + + public function testModelObserversCanBeAttachedToModelsThroughAnArrayUsingAttribute() + { + EloquentModelWithObserveAttributeUsingArrayStub::setEventDispatcher($events = m::mock(Dispatcher::class)); + $events->shouldReceive('dispatch'); + $events->shouldReceive('listen')->once()->with('eloquent.creating: Illuminate\Tests\Database\EloquentModelWithObserveAttributeUsingArrayStub', EloquentTestObserverStub::class.'@creating'); + $events->shouldReceive('listen')->once()->with('eloquent.saved: Illuminate\Tests\Database\EloquentModelWithObserveAttributeUsingArrayStub', EloquentTestObserverStub::class.'@saved'); + $events->shouldReceive('forget'); + EloquentModelWithObserveAttributeUsingArrayStub::flushEventListeners(); + } + public function testThrowExceptionOnAttachingNotExistsModelObserverWithString() { $this->expectException(InvalidArgumentException::class); @@ -2839,6 +2860,17 @@ public function testDiscardChanges() $this->assertNull($user->getOriginal('name')); $this->assertNull($user->getAttribute('name')); } + + public function testModelToJsonSucceedsWithPriorErrors(): void + { + $user = new EloquentModelStub(['name' => 'Mateus']); + + // Simulate a JSON error + json_decode('{'); + $this->assertTrue(json_last_error() !== JSON_ERROR_NONE); + + $this->assertSame('{"name":"Mateus"}', $user->toJson(JSON_THROW_ON_ERROR)); + } } class EloquentTestObserverStub @@ -3334,6 +3366,18 @@ public function uniqueIds() } } +#[ObservedBy(EloquentTestObserverStub::class)] +class EloquentModelWithObserveAttributeStub extends EloquentModelStub +{ + // +} + +#[ObservedBy([EloquentTestObserverStub::class])] +class EloquentModelWithObserveAttributeUsingArrayStub extends EloquentModelStub +{ + // +} + class EloquentModelSavingEventStub { // diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 011e22c99afb..abd2fcc2930c 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -23,6 +23,7 @@ use Illuminate\Pagination\Cursor; use Illuminate\Pagination\CursorPaginator; use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Tests\Database\Fixtures\Enums\Bar; use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -1039,8 +1040,10 @@ public function testEmptyWhereNotIns() public function testWhereIntegerInRaw() { $builder = $this->getBuilder(); - $builder->select('*')->from('users')->whereIntegerInRaw('id', ['1a', 2]); - $this->assertSame('select * from "users" where "id" in (1, 2)', $builder->toSql()); + $builder->select('*')->from('users')->whereIntegerInRaw('id', [ + '1a', 2, Bar::FOO, + ]); + $this->assertSame('select * from "users" where "id" in (1, 2, 5)', $builder->toSql()); $this->assertEquals([], $builder->getBindings()); $builder = $this->getBuilder(); @@ -1048,8 +1051,9 @@ public function testWhereIntegerInRaw() ['id' => '1a'], ['id' => 2], ['any' => '3'], + ['id' => Bar::FOO], ]); - $this->assertSame('select * from "users" where "id" in (1, 2, 3)', $builder->toSql()); + $this->assertSame('select * from "users" where "id" in (1, 2, 3, 5)', $builder->toSql()); $this->assertEquals([], $builder->getBindings()); } @@ -1185,6 +1189,68 @@ public function testWhereFulltextPostgres() $this->assertEquals(['Car Plane'], $builder->getBindings()); } + public function testWhereAll() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->whereAll(['last_name', 'email'], '%Otwell%'); + $this->assertSame('select * from "users" where ("last_name" = ? and "email" = ?)', $builder->toSql()); + $this->assertEquals(['%Otwell%', '%Otwell%'], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->whereAll(['last_name', 'email'], 'not like', '%Otwell%'); + $this->assertSame('select * from "users" where ("last_name" not like ? and "email" not like ?)', $builder->toSql()); + $this->assertEquals(['%Otwell%', '%Otwell%'], $builder->getBindings()); + } + + public function testOrWhereAll() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('first_name', 'like', '%Taylor%')->orWhereAll(['last_name', 'email'], 'like', '%Otwell%'); + $this->assertSame('select * from "users" where "first_name" like ? or ("last_name" like ? and "email" like ?)', $builder->toSql()); + $this->assertEquals(['%Taylor%', '%Otwell%', '%Otwell%'], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('first_name', 'like', '%Taylor%')->whereAll(['last_name', 'email'], 'like', '%Otwell%', 'or'); + $this->assertSame('select * from "users" where "first_name" like ? or ("last_name" like ? and "email" like ?)', $builder->toSql()); + $this->assertEquals(['%Taylor%', '%Otwell%', '%Otwell%'], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('first_name', 'like', '%Taylor%')->orWhereAll(['last_name', 'email'], '%Otwell%'); + $this->assertSame('select * from "users" where "first_name" like ? or ("last_name" = ? and "email" = ?)', $builder->toSql()); + $this->assertEquals(['%Taylor%', '%Otwell%', '%Otwell%'], $builder->getBindings()); + } + + public function testWhereAny() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->whereAny(['last_name', 'email'], 'like', '%Otwell%'); + $this->assertSame('select * from "users" where ("last_name" like ? or "email" like ?)', $builder->toSql()); + $this->assertEquals(['%Otwell%', '%Otwell%'], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->whereAny(['last_name', 'email'], '%Otwell%'); + $this->assertSame('select * from "users" where ("last_name" = ? or "email" = ?)', $builder->toSql()); + $this->assertEquals(['%Otwell%', '%Otwell%'], $builder->getBindings()); + } + + public function testOrWhereAny() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('first_name', 'like', '%Taylor%')->orWhereAny(['last_name', 'email'], 'like', '%Otwell%'); + $this->assertSame('select * from "users" where "first_name" like ? or ("last_name" like ? or "email" like ?)', $builder->toSql()); + $this->assertEquals(['%Taylor%', '%Otwell%', '%Otwell%'], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('first_name', 'like', '%Taylor%')->whereAny(['last_name', 'email'], 'like', '%Otwell%', 'or'); + $this->assertSame('select * from "users" where "first_name" like ? or ("last_name" like ? or "email" like ?)', $builder->toSql()); + $this->assertEquals(['%Taylor%', '%Otwell%', '%Otwell%'], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('first_name', 'like', '%Taylor%')->orWhereAny(['last_name', 'email'], '%Otwell%'); + $this->assertSame('select * from "users" where "first_name" like ? or ("last_name" = ? or "email" = ?)', $builder->toSql()); + $this->assertEquals(['%Taylor%', '%Otwell%', '%Otwell%'], $builder->getBindings()); + } + public function testUnions() { $builder = $this->getBuilder(); @@ -2004,6 +2070,34 @@ public function testGetCountForPaginationWithUnion() $this->assertEquals(1, $count); } + public function testGetCountForPaginationWithUnionOrders() + { + $builder = $this->getBuilder(); + $builder->from('posts')->select('id')->union($this->getBuilder()->from('videos')->select('id'))->latest(); + + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true)->andReturn([['aggregate' => 1]]); + $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { + return $results; + }); + + $count = $builder->getCountForPagination(); + $this->assertEquals(1, $count); + } + + public function testGetCountForPaginationWithUnionLimitAndOffset() + { + $builder = $this->getBuilder(); + $builder->from('posts')->select('id')->union($this->getBuilder()->from('videos')->select('id'))->take(15)->skip(1); + + $builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from ((select "id" from "posts") union (select "id" from "videos")) as "temp_table"', [], true)->andReturn([['aggregate' => 1]]); + $builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function ($builder, $results) { + return $results; + }); + + $count = $builder->getCountForPagination(); + $this->assertEquals(1, $count); + } + public function testWhereShortcut() { $builder = $this->getBuilder(); @@ -2526,6 +2620,117 @@ public function testRightJoinSub() $builder->from('users')->rightJoinSub(['foo'], 'sub', 'users.id', '=', 'sub.id'); } + public function testJoinLateral() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral('select * from `contacts` where `contracts`.`user_id` = `users`.`id`', 'sub'); + $this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $sub = $this->getMySqlBuilder(); + $sub->getConnection()->shouldReceive('getDatabaseName'); + $eloquentBuilder = new EloquentBuilder($sub->from('contacts')->whereColumn('contracts.user_id', 'users.id')); + $builder->from('users')->joinLateral($eloquentBuilder, 'sub'); + $this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $sub1 = $this->getMySqlBuilder(); + $sub1->getConnection()->shouldReceive('getDatabaseName'); + $sub1 = $sub1->from('contacts')->whereColumn('contracts.user_id', 'users.id')->where('name', 'foo'); + + $sub2 = $this->getMySqlBuilder(); + $sub2->getConnection()->shouldReceive('getDatabaseName'); + $sub2 = $sub2->from('contacts')->whereColumn('contracts.user_id', 'users.id')->where('name', 'bar'); + + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral($sub1, 'sub1')->joinLateral($sub2, 'sub2'); + + $expected = 'select * from `users` '; + $expected .= 'inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id` and `name` = ?) as `sub1` on true '; + $expected .= 'inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id` and `name` = ?) as `sub2` on true'; + + $this->assertEquals($expected, $builder->toSql()); + $this->assertEquals(['foo', 'bar'], $builder->getRawBindings()['join']); + + $this->expectException(InvalidArgumentException::class); + $builder = $this->getMySqlBuilder(); + $builder->from('users')->joinLateral(['foo'], 'sub'); + } + + public function testJoinLateralSQLite() + { + $this->expectException(RuntimeException::class); + $builder = $this->getSQLiteBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub')->toSql(); + } + + public function testJoinLateralPostgres() + { + $builder = $this->getPostgresBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from "users" inner join lateral (select * from "contacts" where "contracts"."user_id" = "users"."id") as "sub" on true', $builder->toSql()); + } + + public function testJoinLateralSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->joinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from [users] cross apply (select * from [contacts] where [contracts].[user_id] = [users].[id]) as [sub]', $builder->toSql()); + } + + public function testJoinLateralWithPrefix() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->getGrammar()->setTablePrefix('prefix_'); + $builder->from('users')->joinLateral('select * from `contacts` where `contracts`.`user_id` = `users`.`id`', 'sub'); + $this->assertSame('select * from `prefix_users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `prefix_sub` on true', $builder->toSql()); + } + + public function testLeftJoinLateral() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + + $sub = $this->getMySqlBuilder(); + $sub->getConnection()->shouldReceive('getDatabaseName'); + + $builder->from('users')->leftJoinLateral($sub->from('contacts')->whereColumn('contracts.user_id', 'users.id'), 'sub'); + $this->assertSame('select * from `users` left join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql()); + + $this->expectException(InvalidArgumentException::class); + $builder = $this->getBuilder(); + $builder->from('users')->leftJoinLateral(['foo'], 'sub'); + } + + public function testLeftJoinLateralSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->from('users')->leftJoinLateral(function ($q) { + $q->from('contacts')->whereColumn('contracts.user_id', 'users.id'); + }, 'sub'); + $this->assertSame('select * from [users] outer apply (select * from [contacts] where [contracts].[user_id] = [users].[id]) as [sub]', $builder->toSql()); + } + public function testRawExpressionsInSelect() { $builder = $this->getBuilder(); @@ -2893,6 +3098,137 @@ public function testSqlServerInsertOrIgnoreMethod() $builder->from('users')->insertOrIgnore(['email' => 'foo']); } + public function testInsertOrIgnoreUsingMethod() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('does not support'); + $builder = $this->getBuilder(); + $builder->from('users')->insertOrIgnoreUsing(['email' => 'foo'], 'bar'); + } + + public function testSqlServerInsertOrIgnoreUsingMethod() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('does not support'); + $builder = $this->getSqlServerBuilder(); + $builder->from('users')->insertOrIgnoreUsing(['email' => 'foo'], 'bar'); + } + + public function testMySqlInsertOrIgnoreUsingMethod() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert ignore into `table1` (`foo`) select `bar` from `table2` where `foreign_id` = ?', [0 => 5])->andReturn(1); + + $result = $builder->from('table1')->insertOrIgnoreUsing( + ['foo'], + function (Builder $query) { + $query->select(['bar'])->from('table2')->where('foreign_id', '=', 5); + } + ); + + $this->assertEquals(1, $result); + } + + public function testMySqlInsertOrIgnoreUsingWithEmptyColumns() + { + $builder = $this->getMySqlBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert ignore into `table1` select * from `table2` where `foreign_id` = ?', [0 => 5])->andReturn(1); + + $result = $builder->from('table1')->insertOrIgnoreUsing( + [], + function (Builder $query) { + $query->from('table2')->where('foreign_id', '=', 5); + } + ); + + $this->assertEquals(1, $result); + } + + public function testMySqlInsertOrIgnoreUsingInvalidSubquery() + { + $this->expectException(InvalidArgumentException::class); + $builder = $this->getMySqlBuilder(); + $builder->from('table1')->insertOrIgnoreUsing(['foo'], ['bar']); + } + + public function testPostgresInsertOrIgnoreUsingMethod() + { + $builder = $this->getPostgresBuilder(); + $builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "table1" ("foo") select "bar" from "table2" where "foreign_id" = ? on conflict do nothing', [5])->andReturn(1); + + $result = $builder->from('table1')->insertOrIgnoreUsing( + ['foo'], + function (Builder $query) { + $query->select(['bar'])->from('table2')->where('foreign_id', '=', 5); + } + ); + + $this->assertEquals(1, $result); + } + + public function testPostgresInsertOrIgnoreUsingWithEmptyColumns() + { + $builder = $this->getPostgresBuilder(); + $builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "table1" select * from "table2" where "foreign_id" = ? on conflict do nothing', [5])->andReturn(1); + + $result = $builder->from('table1')->insertOrIgnoreUsing( + [], + function (Builder $query) { + $query->from('table2')->where('foreign_id', '=', 5); + } + ); + + $this->assertEquals(1, $result); + } + + public function testPostgresInsertOrIgnoreUsingInvalidSubquery() + { + $this->expectException(InvalidArgumentException::class); + $builder = $this->getPostgresBuilder(); + $builder->from('table1')->insertOrIgnoreUsing(['foo'], ['bar']); + } + + public function testSQLiteInsertOrIgnoreUsingMethod() + { + $builder = $this->getSQLiteBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert or ignore into "table1" ("foo") select "bar" from "table2" where "foreign_id" = ?', [5])->andReturn(1); + + $result = $builder->from('table1')->insertOrIgnoreUsing( + ['foo'], + function (Builder $query) { + $query->select(['bar'])->from('table2')->where('foreign_id', '=', 5); + } + ); + + $this->assertEquals(1, $result); + } + + public function testSQLiteInsertOrIgnoreUsingWithEmptyColumns() + { + $builder = $this->getSQLiteBuilder(); + $builder->getConnection()->shouldReceive('getDatabaseName'); + $builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert or ignore into "table1" select * from "table2" where "foreign_id" = ?', [5])->andReturn(1); + + $result = $builder->from('table1')->insertOrIgnoreUsing( + [], + function (Builder $query) { + $query->from('table2')->where('foreign_id', '=', 5); + } + ); + + $this->assertEquals(1, $result); + } + + public function testSQLiteInsertOrIgnoreUsingInvalidSubquery() + { + $this->expectException(InvalidArgumentException::class); + $builder = $this->getSQLiteBuilder(); + $builder->from('table1')->insertOrIgnoreUsing(['foo'], ['bar']); + } + public function testInsertGetIdMethod() { $builder = $this->getBuilder(); @@ -3350,6 +3686,23 @@ public function testTruncateMethod() ], $sqlite->compileTruncate($builder)); } + public function testTruncateMethodWithPrefix() + { + $builder = $this->getBuilder(); + $builder->getGrammar()->setTablePrefix('prefix_'); + $builder->getConnection()->shouldReceive('statement')->once()->with('truncate table "prefix_users"', []); + $builder->from('users')->truncate(); + + $sqlite = new SQLiteGrammar; + $sqlite->setTablePrefix('prefix_'); + $builder = $this->getBuilder(); + $builder->from('users'); + $this->assertEquals([ + 'delete from sqlite_sequence where name = ?' => ['prefix_users'], + 'delete from "prefix_users"' => [], + ], $sqlite->compileTruncate($builder)); + } + public function testPreserveAddsClosureToArray() { $builder = $this->getBuilder(); @@ -4951,7 +5304,7 @@ public function testCursorPaginateWithUnionWheres() $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { $this->assertEquals( - '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where ("start_time" > ?)) order by "created_at" asc limit 17', + '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" > ?)) order by "created_at" asc limit 17', $builder->toSql()); $this->assertEquals([$ts], $builder->bindings['where']); $this->assertEquals([$ts], $builder->bindings['union']); @@ -4972,6 +5325,105 @@ public function testCursorPaginateWithUnionWheres() ]), $result); } + public function testCursorPaginateWithMultipleUnionsAndMultipleWheres() + { + $ts = now()->toDateTimeString(); + + $perPage = 16; + $columns = ['test']; + $cursorName = 'cursor-name'; + $cursor = new Cursor(['created_at' => $ts]); + $builder = $this->getMockQueryBuilder(); + $builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos'); + $builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news')->where('extra', 'first')); + $builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'podcast' as type")->from('podcasts')->where('extra', 'second')); + $builder->orderBy('created_at'); + + $builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) { + return new Builder($builder->connection, $builder->grammar, $builder->processor); + }); + + $path = 'http://foo.bar?cursor='.$cursor->encode(); + + $results = collect([ + ['id' => 1, 'created_at' => now(), 'type' => 'video'], + ['id' => 2, 'created_at' => now(), 'type' => 'news'], + ['id' => 3, 'created_at' => now(), 'type' => 'podcasts'], + ]); + + $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { + $this->assertEquals( + '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where "extra" = ? and ("created_at" > ?)) union (select "id", "created_at", \'podcast\' as type from "podcasts" where "extra" = ? and ("created_at" > ?)) order by "created_at" asc limit 17', + $builder->toSql()); + $this->assertEquals([$ts], $builder->bindings['where']); + $this->assertEquals(['first', $ts, 'second', $ts], $builder->bindings['union']); + + return $results; + }); + + Paginator::currentPathResolver(function () use ($path) { + return $path; + }); + + $result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor); + + $this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [ + 'path' => $path, + 'cursorName' => $cursorName, + 'parameters' => ['created_at'], + ]), $result); + } + + public function testCursorPaginateWithUnionMultipleWheresMultipleOrders() + { + $ts = now()->toDateTimeString(); + + $perPage = 16; + $columns = ['id', 'created_at', 'type']; + $cursorName = 'cursor-name'; + $cursor = new Cursor(['id' => 1, 'created_at' => $ts, 'type' => 'news']); + $builder = $this->getMockQueryBuilder(); + $builder->select('id', 'start_time as created_at', 'type')->from('videos')->where('extra', 'first'); + $builder->union($this->getBuilder()->select('id', 'created_at', 'type')->from('news')->where('extra', 'second')); + $builder->union($this->getBuilder()->select('id', 'created_at', 'type')->from('podcasts')->where('extra', 'third')); + $builder->orderBy('id')->orderByDesc('created_at')->orderBy('type'); + + $builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) { + return new Builder($builder->connection, $builder->grammar, $builder->processor); + }); + + $path = 'http://foo.bar?cursor='.$cursor->encode(); + + $results = collect([ + ['id' => 1, 'created_at' => now()->addDay(), 'type' => 'video'], + ['id' => 1, 'created_at' => now(), 'type' => 'news'], + ['id' => 1, 'created_at' => now(), 'type' => 'podcast'], + ['id' => 2, 'created_at' => now(), 'type' => 'podcast'], + ]); + + $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { + $this->assertEquals( + '(select "id", "start_time" as "created_at", "type" from "videos" where "extra" = ? and ("id" > ? or ("id" = ? and ("start_time" < ? or ("start_time" = ? and ("type" > ?)))))) union (select "id", "created_at", "type" from "news" where "extra" = ? and ("id" > ? or ("id" = ? and ("start_time" < ? or ("start_time" = ? and ("type" > ?)))))) union (select "id", "created_at", "type" from "podcasts" where "extra" = ? and ("id" > ? or ("id" = ? and ("start_time" < ? or ("start_time" = ? and ("type" > ?)))))) order by "id" asc, "created_at" desc, "type" asc limit 17', + $builder->toSql()); + $this->assertEquals(['first', 1, 1, $ts, $ts, 'news'], $builder->bindings['where']); + $this->assertEquals(['second', 1, 1, $ts, $ts, 'news', 'third', 1, 1, $ts, $ts, 'news'], $builder->bindings ['union']); + + return $results; + }); + + Paginator::currentPathResolver(function () use ($path) { + return $path; + }); + + $result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor); + + $this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [ + 'path' => $path, + 'cursorName' => $cursorName, + 'parameters' => ['id', 'created_at', 'type'], + ]), $result); + } + public function testCursorPaginateWithUnionWheresWithRawOrderExpression() { $ts = now()->toDateTimeString(); @@ -4998,7 +5450,7 @@ public function testCursorPaginateWithUnionWheresWithRawOrderExpression() $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { $this->assertEquals( - '(select "id", "is_published", "start_time" as "created_at", \'video\' as type from "videos" where "is_published" = ? and ("start_time" > ?)) union (select "id", "is_published", "created_at", \'news\' as type from "news" where "is_published" = ? and ("start_time" > ?)) order by case when (id = 3 and type="news" then 0 else 1 end), "created_at" asc limit 17', + '(select "id", "is_published", "start_time" as "created_at", \'video\' as type from "videos" where "is_published" = ? and ("start_time" > ?)) union (select "id", "is_published", "created_at", \'news\' as type from "news" where "is_published" = ? and ("created_at" > ?)) order by case when (id = 3 and type="news" then 0 else 1 end), "created_at" asc limit 17', $builder->toSql()); $this->assertEquals([true, $ts], $builder->bindings['where']); $this->assertEquals([true, $ts], $builder->bindings['union']); @@ -5045,7 +5497,7 @@ public function testCursorPaginateWithUnionWheresReverseOrder() $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { $this->assertEquals( - '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ?)) union (select "id", "created_at", \'news\' as type from "news" where ("start_time" < ?)) order by "created_at" desc limit 17', + '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ?)) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" < ?)) order by "created_at" desc limit 17', $builder->toSql()); $this->assertEquals([$ts], $builder->bindings['where']); $this->assertEquals([$ts], $builder->bindings['union']); @@ -5092,7 +5544,7 @@ public function testCursorPaginateWithUnionWheresMultipleOrders() $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { $this->assertEquals( - '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ? or ("start_time" = ? and ("id" > ?)))) union (select "id", "created_at", \'news\' as type from "news" where ("start_time" < ? or ("start_time" = ? and ("id" > ?)))) order by "created_at" desc, "id" asc limit 17', + '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ? or ("start_time" = ? and ("id" > ?)))) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" < ? or ("created_at" = ? and ("id" > ?)))) order by "created_at" desc, "id" asc limit 17', $builder->toSql()); $this->assertEquals([$ts, $ts, 1], $builder->bindings['where']); $this->assertEquals([$ts, $ts, 1], $builder->bindings['union']); @@ -5113,6 +5565,55 @@ public function testCursorPaginateWithUnionWheresMultipleOrders() ]), $result); } + public function testCursorPaginateWithUnionWheresAndAliassedOrderColumns() + { + $ts = now()->toDateTimeString(); + + $perPage = 16; + $columns = ['test']; + $cursorName = 'cursor-name'; + $cursor = new Cursor(['created_at' => $ts]); + $builder = $this->getMockQueryBuilder(); + $builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos'); + $builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news')); + $builder->union($this->getBuilder()->select('id', 'init_at as created_at')->selectRaw("'podcast' as type")->from('podcasts')); + $builder->orderBy('created_at'); + + $builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) { + return new Builder($builder->connection, $builder->grammar, $builder->processor); + }); + + $path = 'http://foo.bar?cursor='.$cursor->encode(); + + $results = collect([ + ['id' => 1, 'created_at' => now(), 'type' => 'video'], + ['id' => 2, 'created_at' => now(), 'type' => 'news'], + ['id' => 3, 'created_at' => now(), 'type' => 'podcast'], + ]); + + $builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) { + $this->assertEquals( + '(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where ("created_at" > ?)) union (select "id", "init_at" as "created_at", \'podcast\' as type from "podcasts" where ("init_at" > ?)) order by "created_at" asc limit 17', + $builder->toSql()); + $this->assertEquals([$ts], $builder->bindings['where']); + $this->assertEquals([$ts, $ts], $builder->bindings['union']); + + return $results; + }); + + Paginator::currentPathResolver(function () use ($path) { + return $path; + }); + + $result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor); + + $this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [ + 'path' => $path, + 'cursorName' => $cursorName, + 'parameters' => ['created_at'], + ]), $result); + } + public function testWhereExpression() { $builder = $this->getBuilder(); diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 60339647c0f9..409c28ccd637 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -164,7 +164,7 @@ public function testRenameIndex() $table->index(['name', 'email'], 'index1'); }); - $indexes = array_column($schema->getIndexes('users'), 'name'); + $indexes = $schema->getIndexListing('users'); $this->assertContains('index1', $indexes); $this->assertNotContains('index2', $indexes); @@ -173,10 +173,8 @@ public function testRenameIndex() $table->renameIndex('index1', 'index2'); }); - $indexes = $schema->getIndexes('users'); - - $this->assertNotContains('index1', array_column($indexes, 'name')); - $this->assertTrue(collect($indexes)->contains( + $this->assertFalse($schema->hasIndex('users', 'index1')); + $this->assertTrue(collect($schema->getIndexes('users'))->contains( fn ($index) => $index['name'] === 'index2' && $index['columns'] === ['name', 'email'] )); } diff --git a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php index ce261ab4e94d..80734d25fef8 100644 --- a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php @@ -87,10 +87,7 @@ public function testHasColumnAndIndexWithPrefixIndexDisabled() $table->string('name')->index(); }); - $this->assertContains( - 'table1_name_index', - array_column($this->db->connection()->getSchemaBuilder()->getIndexes('table1'), 'name') - ); + $this->assertTrue($this->db->connection()->getSchemaBuilder()->hasIndex('table1', 'table1_name_index')); } public function testHasColumnAndIndexWithPrefixIndexEnabled() @@ -107,10 +104,7 @@ public function testHasColumnAndIndexWithPrefixIndexEnabled() $table->string('name')->index(); }); - $this->assertContains( - 'example_table1_name_index', - array_column($this->db->connection()->getSchemaBuilder()->getIndexes('table1'), 'name') - ); + $this->assertTrue($this->db->connection()->getSchemaBuilder()->hasIndex('table1', 'example_table1_name_index')); } public function testDropColumnWithTablePrefix() diff --git a/tests/Database/Fixtures/Enums/Bar.php b/tests/Database/Fixtures/Enums/Bar.php new file mode 100644 index 000000000000..bc019a9be02c --- /dev/null +++ b/tests/Database/Fixtures/Enums/Bar.php @@ -0,0 +1,8 @@ +now = Carbon::create(2017, 6, 27, 13, 14, 15, 'UTC')); + $connection = new Connection(new PDO('sqlite::memory:')); $connection->setEventDispatcher(new Dispatcher()); $called = 0; - $connection->whenQueryingForLongerThan(now()->addMilliseconds(1), function () use (&$called) { + $connection->whenQueryingForLongerThan($this->now->addMilliseconds(1), function () use (&$called) { $called++; }); diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index c2d7922a5518..9dd164585b5d 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -213,6 +213,22 @@ public function testRequestCanPassWithoutRulesMethod() $this->assertEquals([], $request->all()); } + public function testRequestWithGetRules() + { + FoundationTestFormRequestWithGetRules::$useRuleSet = 'a'; + $request = $this->createRequest(['a' => 1], FoundationTestFormRequestWithGetRules::class); + + $request->validateResolved(); + $this->assertEquals(['a' => 1], $request->all()); + + $this->expectException(ValidationException::class); + FoundationTestFormRequestWithGetRules::$useRuleSet = 'b'; + + $request = $this->createRequest(['a' => 1], FoundationTestFormRequestWithGetRules::class); + + $request->validateResolved(); + } + /** * Catch the given exception thrown from the executor, and return it. * @@ -482,3 +498,21 @@ public function authorize() return true; } } + +class FoundationTestFormRequestWithGetRules extends FormRequest +{ + public static $useRuleSet = 'a'; + + protected function validationRules(): array + { + if (self::$useRuleSet === 'a') { + return [ + 'a' => ['required', 'int', 'min:1'], + ]; + } else { + return [ + 'a' => ['required', 'int', 'min:2'], + ]; + } + } +} diff --git a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php index feac68b8e5bc..82ad8b410bee 100644 --- a/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php +++ b/tests/Foundation/Testing/DatabaseTransactionsManagerTest.php @@ -31,6 +31,23 @@ public function testItIgnoresTheBaseTransactionForCallbackApplicableTransactions $this->assertEquals(2, $manager->callbackApplicableTransactions()[0]->level); } + public function testCommittingDoesNotRemoveTheBasePendingTransaction() + { + $manager = new DatabaseTransactionsManager; + + $manager->begin('foo', 1); + + $manager->begin('foo', 2); + $manager->commit('foo', 2, 1); + + $this->assertCount(0, $manager->callbackApplicableTransactions()); + + $manager->begin('foo', 2); + + $this->assertCount(1, $manager->callbackApplicableTransactions()); + $this->assertEquals(2, $manager->callbackApplicableTransactions()[0]->level); + } + public function testItExecutesCallbacksForTheSecondTransaction() { $testObject = new TestingDatabaseTransactionsManagerTestObject(); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index df4f7165373c..ea204c8c3f02 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -6,6 +6,7 @@ use GuzzleHttp\Middleware; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response as Psr7Response; +use GuzzleHttp\Psr7\Utils; use GuzzleHttp\TransferStats; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Support\Arrayable; @@ -357,6 +358,26 @@ public function testSendRequestBodyWithManyAmpersands() $this->factory->withBody($body, 'text/plain')->send('post', 'http://foo.com/api'); } + public function testSendStreamRequestBody() + { + $string = 'Look at me, i am a stream!!'; + $resource = fopen('php://temp', 'w'); + fwrite($resource, $string); + rewind($resource); + $body = Utils::streamFor($resource); + + $fakeRequest = function (Request $request) use ($string) { + self::assertSame($string, $request->body()); + self::assertContains('text/plain', $request->header('Content-Type')); + + return ['my' => 'response']; + }; + + $this->factory->fake($fakeRequest); + + $this->factory->withBody($body, 'text/plain')->send('post', 'http://foo.com/api'); + } + public function testUrlsCanBeStubbedByPath() { $this->factory->fake([ @@ -1690,6 +1711,28 @@ public function testRequestExceptionIsThrownWhenRetriesExhausted() $this->factory->assertSentCount(2); } + public function testRequestExceptionIsThrownWhenRetriesExhaustedWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + $exception = null; + + try { + $this->factory + ->retry([1], 0, null, true) + ->get('http://foo.com/get'); + } catch (RequestException $e) { + $exception = $e; + } + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + + $this->factory->assertSentCount(2); + } + public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ @@ -1719,6 +1762,35 @@ public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessary() $this->factory->assertSentCount(1); } + public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessaryWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 500), + ]); + + $exception = null; + $whenAttempts = 0; + + try { + $this->factory + ->retry([1000, 1000], 1000, function ($exception) use (&$whenAttempts) { + $whenAttempts++; + + return $exception->response->status() === 403; + }, true) + ->get('http://foo.com/get'); + } catch (RequestException $e) { + $exception = $e; + } + + $this->assertNotNull($exception); + $this->assertInstanceOf(RequestException::class, $exception); + + $this->assertSame(1, $whenAttempts); + + $this->factory->assertSentCount(1); + } + public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhausted() { $this->factory->fake([ @@ -1734,6 +1806,21 @@ public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhausted() $this->factory->assertSentCount(2); } + public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhaustedWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 403), + ]); + + $response = $this->factory + ->retry([1, 2], throw: false) + ->get('http://foo.com/get'); + + $this->assertTrue($response->failed()); + + $this->factory->assertSentCount(3); + } + public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ @@ -1757,6 +1844,29 @@ public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessary $this->factory->assertSentCount(1); } + public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessaryWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 500), + ]); + + $whenAttempts = 0; + + $response = $this->factory + ->retry([1, 2], 0, function ($exception) use (&$whenAttempts) { + $whenAttempts++; + + return $exception->response->status() === 403; + }, false) + ->get('http://foo.com/get'); + + $this->assertTrue($response->failed()); + + $this->assertSame(1, $whenAttempts); + + $this->factory->assertSentCount(1); + } + public function testRequestCanBeModifiedInRetryCallback() { $this->factory->fake([ @@ -1782,6 +1892,31 @@ public function testRequestCanBeModifiedInRetryCallback() }); } + public function testRequestCanBeModifiedInRetryCallbackWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory->sequence() + ->push(['error'], 500) + ->push(['ok'], 200), + ]); + + $response = $this->factory + ->retry([2], when: function ($exception, $request) { + $this->assertInstanceOf(PendingRequest::class, $request); + + $request->withHeaders(['Foo' => 'Bar']); + + return true; + }, throw: false) + ->get('http://foo.com/get'); + + $this->assertTrue($response->successful()); + + $this->factory->assertSent(function (Request $request) { + return $request->hasHeader('Foo') && $request->header('Foo') === ['Bar']; + }); + } + public function testExceptionThrownInRetryCallbackWithoutRetrying() { $this->factory->fake([ @@ -1807,6 +1942,31 @@ public function testExceptionThrownInRetryCallbackWithoutRetrying() $this->factory->assertSentCount(1); } + public function testExceptionThrownInRetryCallbackWithoutRetryingWithBackoffArray() + { + $this->factory->fake([ + '*' => $this->factory->response(['error'], 500), + ]); + + $exception = null; + + try { + $this->factory + ->retry([1, 2, 3], when: function ($exception) use (&$whenAttempts) { + throw new Exception('Foo bar'); + }, throw: false) + ->get('http://foo.com/get'); + } catch (Exception $e) { + $exception = $e; + } + + $this->assertNotNull($exception); + $this->assertInstanceOf(Exception::class, $exception); + $this->assertEquals('Foo bar', $exception->getMessage()); + + $this->factory->assertSentCount(1); + } + public function testRequestsWillBeWaitingSleepMillisecondsReceivedBeforeRetry() { Sleep::fake(); @@ -1837,6 +1997,34 @@ public function testRequestsWillBeWaitingSleepMillisecondsReceivedBeforeRetry() ]); } + public function testRequestsWillBeWaitingSleepMillisecondsReceivedInBackoffArray() + { + Sleep::fake(); + + $this->factory->fake([ + '*' => $this->factory->sequence() + ->push(['error'], 500) + ->push(['error'], 500) + ->push(['error'], 500) + ->push(['ok'], 200), + ]); + + $this->factory + ->retry([50, 100, 200], 0, null, true) + ->get('http://foo.com/get'); + + $this->factory->assertSentCount(4); + + // Make sure we waited 300ms for the first two attempts + Sleep::assertSleptTimes(3); + + Sleep::assertSequence([ + Sleep::usleep(50_000), + Sleep::usleep(100_000), + Sleep::usleep(200_000), + ]); + } + public function testMiddlewareRunsWhenFaked() { $this->factory->fake(function (Request $request) { @@ -2659,6 +2847,53 @@ public function testItCanReturnCustomResponseClass(): void $this->assertInstanceOf(TestResponse::class, $response); $this->assertSame('expected content', $response->body()); } + + public function testItCanHaveGlobalDefaultValues() + { + $factory = new Factory; + $timeout = null; + $allowRedirects = null; + $headers = null; + $factory->fake(function ($request, $options) use (&$timeout, &$allowRedirects, &$headers, $factory) { + $timeout = $options['timeout']; + $allowRedirects = $options['allow_redirects']; + $headers = $request->headers(); + + return $factory->response(''); + }); + + $factory->get('https://laravel.com'); + $this->assertSame(30, $timeout); + $this->assertSame(['max' => 5, 'protocols' => ['http', 'https'], 'strict' => false, 'referer' => false, 'track_redirects' => false], $allowRedirects); + $this->assertNull($headers['X-Foo'] ?? null); + + $factory->globalOptions([ + 'timeout' => 5, + 'allow_redirects' => false, + 'headers' => [ + 'X-Foo' => 'true', + ], + ]); + + $factory->get('https://laravel.com'); + $this->assertSame(5, $timeout); + $this->assertFalse($allowRedirects); + $this->assertSame(['true'], $headers['X-Foo']); + + $factory->globalOptions([ + 'timeout' => 10, + 'headers' => [ + 'X-Foo' => 'false', + 'X-Bar' => 'true', + ], + ]); + + $factory->get('https://laravel.com'); + $this->assertSame(10, $timeout); + $this->assertSame(['max' => 5, 'protocols' => ['http', 'https'], 'strict' => false, 'referer' => false, 'track_redirects' => false], $allowRedirects); + $this->assertSame(['false'], $headers['X-Foo']); + $this->assertSame(['true'], $headers['X-Bar']); + } } class CustomFactory extends Factory diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 75a0cf5571af..b0d3f53e1242 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -1608,4 +1608,14 @@ public function testItCanHaveObjectsInJsonPayload() $this->assertSame(['name' => 'Laravel'], $request->get('framework')); } + + public function testItDoesNotGenerateJsonErrorsForEmptyContent() + { + // clear any existing errors + json_encode(null); + + Request::create('', 'GET')->json(); + + $this->assertTrue(json_last_error() === JSON_ERROR_NONE); + } } diff --git a/tests/Http/JsonResourceTest.php b/tests/Http/JsonResourceTest.php new file mode 100644 index 000000000000..256c40aea991 --- /dev/null +++ b/tests/Http/JsonResourceTest.php @@ -0,0 +1,27 @@ + $model]) + ->makePartial() + ->shouldReceive('jsonSerialize')->once()->andReturn(['foo' => 'bar']) + ->getMock(); + + // Simulate a JSON error + json_decode('{'); + $this->assertTrue(json_last_error() !== JSON_ERROR_NONE); + + $this->assertSame('{"foo":"bar"}', $resource->toJson(JSON_THROW_ON_ERROR)); + } +} diff --git a/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php b/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php index c5fc77aec88d..ca09a8c89213 100644 --- a/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php +++ b/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php @@ -3,21 +3,18 @@ namespace Illuminate\Tests\Integration\Auth; use Illuminate\Database\QueryException; -use Illuminate\Foundation\Auth\User as FoundationUser; use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_mysql - */ +#[RequiresPhpExtension('pdo_mysql')] class ApiAuthenticationWithEloquentTest extends TestCase { - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { // Auth configuration $app['config']->set('auth.defaults.guard', 'api'); - $app['config']->set('auth.providers.users.model', User::class); $app['config']->set('auth.guards.api', [ 'driver' => 'token', @@ -59,8 +56,3 @@ public function testAuthenticationViaApiWithEloquentUsingWrongDatabaseCredential } } } - -class User extends FoundationUser -{ - // -} diff --git a/tests/Integration/Auth/AuthenticationTest.php b/tests/Integration/Auth/AuthenticationTest.php index 388f23d030be..cd65dd793145 100644 --- a/tests/Integration/Auth/AuthenticationTest.php +++ b/tests/Integration/Auth/AuthenticationTest.php @@ -13,6 +13,7 @@ use Illuminate\Auth\SessionGuard; use Illuminate\Database\Schema\Blueprint; use Illuminate\Events\Dispatcher; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; @@ -20,27 +21,42 @@ use Illuminate\Support\Testing\Fakes\EventFake; use Illuminate\Tests\Integration\Auth\Fixtures\AuthenticationTestUser; use InvalidArgumentException; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; +#[WithMigration] class AuthenticationTest extends TestCase { - protected function getEnvironmentSetUp($app) + use RefreshDatabase; + + protected function defineEnvironment($app) { - $app['config']->set('auth.providers.users.model', AuthenticationTestUser::class); + $app['config']->set([ + 'auth.providers.users.model' => AuthenticationTestUser::class, + 'hashing.driver' => 'bcrypt', + ]); + } - $app['config']->set('hashing', ['driver' => 'bcrypt']); + protected function defineRoutes($router) + { + $router->get('basic', function () { + return $this->app['auth']->guard()->basic() + ?: $this->app['auth']->user()->toJson(); + }); + + $router->get('basicWithCondition', function () { + return $this->app['auth']->guard()->basic('email', ['is_active' => true]) + ?: $this->app['auth']->user()->toJson(); + }); } - protected function setUp(): void + protected function afterRefreshingDatabase() { - parent::setUp(); - - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('email'); - $table->string('username'); - $table->string('password'); - $table->string('remember_token')->default(null)->nullable(); + Schema::table('users', function (Blueprint $table) { + $table->renameColumn('name', 'username'); + }); + + Schema::table('users', function (Blueprint $table) { $table->tinyInteger('is_active')->default(0); }); @@ -50,16 +66,6 @@ protected function setUp(): void 'password' => bcrypt('password'), 'is_active' => true, ]); - - $this->app->make('router')->get('basic', function () { - return $this->app['auth']->guard()->basic() - ?: $this->app['auth']->user()->toJson(); - }); - - $this->app->make('router')->get('basicWithCondition', function () { - return $this->app['auth']->guard()->basic('email', ['is_active' => true]) - ?: $this->app['auth']->user()->toJson(); - }); } public function testBasicAuthProtectsRoute() diff --git a/tests/Integration/Auth/ForgotPasswordTest.php b/tests/Integration/Auth/ForgotPasswordTest.php index 46a682c229d8..1bdee928b130 100644 --- a/tests/Integration/Auth/ForgotPasswordTest.php +++ b/tests/Integration/Auth/ForgotPasswordTest.php @@ -3,16 +3,21 @@ namespace Illuminate\Tests\Integration\Auth; use Illuminate\Auth\Notifications\ResetPassword; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; use Illuminate\Tests\Integration\Auth\Fixtures\AuthenticationTestUser; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; +#[WithMigration] class ForgotPasswordTest extends TestCase { + use RefreshDatabase; + protected function tearDown(): void { ResetPassword::$createUrlCallback = null; @@ -27,11 +32,6 @@ protected function defineEnvironment($app) $app['config']->set('auth.providers.users.model', AuthenticationTestUser::class); } - protected function defineDatabaseMigrations() - { - $this->loadLaravelMigrations(); - } - protected function defineRoutes($router) { $router->get('password/reset/{token}', function ($token) { diff --git a/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php b/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php index e34dcd78b82d..f5f680beb1bc 100644 --- a/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php +++ b/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php @@ -3,16 +3,21 @@ namespace Illuminate\Tests\Integration\Auth; use Illuminate\Auth\Notifications\ResetPassword; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; use Illuminate\Tests\Integration\Auth\Fixtures\AuthenticationTestUser; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; +#[WithMigration] class ForgotPasswordWithoutDefaultRoutesTest extends TestCase { + use RefreshDatabase; + protected function tearDown(): void { ResetPassword::$createUrlCallback = null; @@ -27,11 +32,6 @@ protected function defineEnvironment($app) $app['config']->set('auth.providers.users.model', AuthenticationTestUser::class); } - protected function defineDatabaseMigrations() - { - $this->loadLaravelMigrations(); - } - protected function defineRoutes($router) { $router->get('custom/password/reset/{token}', function ($token) { diff --git a/tests/Integration/Auth/RehashOnLogoutOtherDevicesTest.php b/tests/Integration/Auth/RehashOnLogoutOtherDevicesTest.php new file mode 100644 index 000000000000..8e704ff33345 --- /dev/null +++ b/tests/Integration/Auth/RehashOnLogoutOtherDevicesTest.php @@ -0,0 +1,46 @@ +post('logout', function (Request $request) { + auth()->logoutOtherDevices($request->input('password')); + + return response()->noContent(); + })->middleware(['web', 'auth']); + } + + public function testItRehashThePasswordUsingLogoutOtherDevices() + { + $this->withoutExceptionHandling(); + + $user = UserFactory::new()->create(); + + $password = $user->password; + + $this->actingAs($user); + + $this->post('logout', [ + 'password' => 'password', + ])->assertStatus(204); + + $user->refresh(); + + $this->assertNotSame($password, $user->password); + } +} diff --git a/tests/Integration/Cache/DynamoDbStoreTest.php b/tests/Integration/Cache/DynamoDbStoreTest.php index e59ebbfd774b..b465cc61ea0e 100644 --- a/tests/Integration/Cache/DynamoDbStoreTest.php +++ b/tests/Integration/Cache/DynamoDbStoreTest.php @@ -7,19 +7,12 @@ use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; +use Orchestra\Testbench\Attributes\RequiresEnv; use Orchestra\Testbench\TestCase; +#[RequiresEnv('DYNAMODB_CACHE_TABLE')] class DynamoDbStoreTest extends TestCase { - protected function setUp(): void - { - if (! env('DYNAMODB_CACHE_TABLE')) { - $this->markTestSkipped('DynamoDB not configured.'); - } - - parent::setUp(); - } - public function testItemsCanBeStoredAndRetrieved() { Cache::driver('dynamodb')->put('name', 'Taylor', 10); diff --git a/tests/Integration/Cache/FileCacheLockTest.php b/tests/Integration/Cache/FileCacheLockTest.php index c00409fe9e0e..594471806393 100644 --- a/tests/Integration/Cache/FileCacheLockTest.php +++ b/tests/Integration/Cache/FileCacheLockTest.php @@ -4,21 +4,12 @@ use Exception; use Illuminate\Support\Facades\Cache; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +#[WithConfig('cache.default', 'file')] class FileCacheLockTest extends TestCase { - /** - * Define environment setup. - * - * @param \Illuminate\Foundation\Application $app - * @return void - */ - protected function getEnvironmentSetUp($app) - { - $app['config']->set('cache.default', 'file'); - } - public function testLocksCanBeAcquiredAndReleased() { Cache::lock('foo')->forceRelease(); diff --git a/tests/Integration/Cache/MemcachedCacheLockTestCase.php b/tests/Integration/Cache/MemcachedCacheLockTestCase.php index 8dbbd481f17e..e4fc82dca804 100644 --- a/tests/Integration/Cache/MemcachedCacheLockTestCase.php +++ b/tests/Integration/Cache/MemcachedCacheLockTestCase.php @@ -4,10 +4,9 @@ use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Support\Facades\Cache; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension memcached - */ +#[RequiresPhpExtension('memcached')] class MemcachedCacheLockTestCase extends MemcachedIntegrationTestCase { public function testMemcachedLocksCanBeAcquiredAndReleased() diff --git a/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php b/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php index 4aab9422a8fd..ee7787aa407e 100644 --- a/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php +++ b/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php @@ -3,10 +3,9 @@ namespace Illuminate\Tests\Integration\Cache; use Illuminate\Support\Facades\Cache; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension memcached - */ +#[RequiresPhpExtension('memcached')] class MemcachedTaggedCacheTestCase extends MemcachedIntegrationTestCase { public function testMemcachedCanStoreAndRetrieveTaggedCacheItems() diff --git a/tests/Integration/Cache/NoLockTest.php b/tests/Integration/Cache/NoLockTest.php index 9f1fc983b3aa..3851ea1278c2 100644 --- a/tests/Integration/Cache/NoLockTest.php +++ b/tests/Integration/Cache/NoLockTest.php @@ -3,27 +3,13 @@ namespace Illuminate\Tests\Integration\Cache; use Illuminate\Support\Facades\Cache; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; +#[WithConfig('cache.default', 'null')] +#[WithConfig('cache.stores.null', ['driver' => 'null'])] class NoLockTest extends TestCase { - /** - * Define environment setup. - * - * @param \Illuminate\Foundation\Application $app - * @return void - */ - protected function getEnvironmentSetUp($app) - { - $app['config']->set('cache.default', 'null'); - - $app['config']->set('cache.stores', [ - 'null' => [ - 'driver' => 'null', - ], - ]); - } - public function testLocksCanAlwaysBeAcquiredAndReleased() { Cache::lock('foo')->forceRelease(); diff --git a/tests/Integration/Cache/PhpRedisCacheLockTest.php b/tests/Integration/Cache/PhpRedisCacheLockTest.php index 0ca4ea85f960..63016545fdcf 100644 --- a/tests/Integration/Cache/PhpRedisCacheLockTest.php +++ b/tests/Integration/Cache/PhpRedisCacheLockTest.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Support\Facades\Cache; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Redis; class PhpRedisCacheLockTest extends TestCase @@ -138,9 +139,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithMsgpackSerialization() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension lzf - */ + #[RequiresPhpExtension('lzf')] public function testRedisLockCanBeAcquiredAndReleasedWithLzfCompression() { if (! defined('Redis::COMPRESSION_LZF')) { @@ -167,9 +166,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithLzfCompression() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension zstd - */ + #[RequiresPhpExtension('zstd')] public function testRedisLockCanBeAcquiredAndReleasedWithZstdCompression() { if (! defined('Redis::COMPRESSION_ZSTD')) { @@ -215,9 +212,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithZstdCompression() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension lz4 - */ + #[RequiresPhpExtension('lz4')] public function testRedisLockCanBeAcquiredAndReleasedWithLz4Compression() { if (! defined('Redis::COMPRESSION_LZ4')) { @@ -263,9 +258,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithLz4Compression() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension Lzf - */ + #[RequiresPhpExtension('Lzf')] public function testRedisLockCanBeAcquiredAndReleasedWithSerializationAndCompression() { if (! defined('Redis::COMPRESSION_LZF')) { diff --git a/tests/Integration/Cache/Psr6RedisTest.php b/tests/Integration/Cache/Psr6RedisTest.php index a242a2f77e29..062d5ec4dbfb 100644 --- a/tests/Integration/Cache/Psr6RedisTest.php +++ b/tests/Integration/Cache/Psr6RedisTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Tests\Integration\Cache\Fixtures\Unserializable; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class Psr6RedisTest extends TestCase { @@ -25,9 +26,7 @@ protected function tearDown(): void $this->tearDownRedis(); } - /** - * @dataProvider redisClientDataProvider - */ + #[DataProvider('redisClientDataProvider')] public function testTransactionIsNotOpenedWhenSerializationFails($redisClient): void { $this->app['config']['cache.default'] = 'redis'; diff --git a/tests/Integration/Cache/RedisStoreTest.php b/tests/Integration/Cache/RedisStoreTest.php index c8c7abb13f62..1d748cf24525 100644 --- a/tests/Integration/Cache/RedisStoreTest.php +++ b/tests/Integration/Cache/RedisStoreTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Cache; +use DateTime; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Redis; @@ -75,6 +76,20 @@ public function testItCanStoreNan() $this->assertNan(Cache::store('redis')->get('foo')); } + public function testItCanExpireWithZeroTTL() + { + Cache::store('redis')->clear(); + + $result = Cache::store('redis')->put('foo', 10, 10); + $this->assertTrue($result); + + $result = Cache::store('redis')->put('foo', 10, 0); + $this->assertTrue($result); + + $value = Cache::store('redis')->get('foo'); + $this->assertNull($value); + } + public function testTagsCanBeAccessed() { Cache::store('redis')->clear(); @@ -139,6 +154,30 @@ public function testIncrementedTagEntriesProperlyTurnStale() $this->assertEquals(0, count($keyCount)); } + public function testPastTtlTagEntriesAreNotAdded() + { + Cache::store('redis')->clear(); + + Cache::store('redis')->tags(['votes'])->add('person-1', 0, new DateTime('yesterday')); + + $value = Cache::store('redis')->tags(['votes'])->get('person-1'); + $this->assertNull($value); + + $keyCount = Cache::store('redis')->connection()->keys('*'); + $this->assertEquals(0, count($keyCount)); + } + + public function testPutPastTtlTagEntriesProperlyTurnStale() + { + Cache::store('redis')->clear(); + + Cache::store('redis')->tags(['votes'])->put('person-1', 0, new DateTime('yesterday')); + Cache::store('redis')->tags(['votes'])->flushStale(); + + $keyCount = Cache::store('redis')->connection()->keys('*'); + $this->assertEquals(0, count($keyCount)); + } + public function testTagsCanBeFlushedBySingleKey() { Cache::store('redis')->clear(); diff --git a/tests/Integration/Console/CallCommandsTest.php b/tests/Integration/Console/CallCommandsTest.php new file mode 100644 index 000000000000..4724b538aec1 --- /dev/null +++ b/tests/Integration/Console/CallCommandsTest.php @@ -0,0 +1,38 @@ +afterApplicationCreated(function () { + Artisan::command('test:a', function () { + $this->call('view:clear'); + }); + + Artisan::command('test:b', function () { + $this->call(ViewClearCommand::class); + }); + + Artisan::command('test:c', function () { + $this->call($this->laravel->make(ViewClearCommand::class)); + }); + }); + + parent::setUp(); + } + + #[TestWith(['test:a'])] + #[TestWith(['test:b'])] + #[TestWith(['test:c'])] + public function testItCanCallCommands(string $command) + { + $this->artisan($command)->assertSuccessful(); + } +} diff --git a/tests/Integration/Console/CommandEventsTest.php b/tests/Integration/Console/CommandEventsTest.php index 167835eb9b7f..cc63e22dc4fc 100644 --- a/tests/Integration/Console/CommandEventsTest.php +++ b/tests/Integration/Console/CommandEventsTest.php @@ -12,6 +12,7 @@ use Illuminate\Support\Str; use Orchestra\Testbench\Foundation\Application as Testbench; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class CommandEventsTest extends TestCase { @@ -48,9 +49,7 @@ protected function setUp(): void parent::setUp(); } - /** - * @dataProvider foregroundCommandEventsProvider - */ + #[DataProvider('foregroundCommandEventsProvider')] public function testCommandEventsReceiveParsedInput($callback) { $this->app[ConsoleKernel::class]->registerCommand(new CommandEventsTestCommand); diff --git a/tests/Integration/Console/CommandSchedulingTest.php b/tests/Integration/Console/CommandSchedulingTest.php index 1e02c3b3d2fa..1a4c918aa5e0 100644 --- a/tests/Integration/Console/CommandSchedulingTest.php +++ b/tests/Integration/Console/CommandSchedulingTest.php @@ -6,6 +6,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class CommandSchedulingTest extends TestCase { @@ -62,9 +63,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @dataProvider executionProvider - */ + #[DataProvider('executionProvider')] public function testExecutionOrder($background, $expected) { $schedule = $this->app->make(Schedule::class); diff --git a/tests/Integration/Console/ConsoleApplicationTest.php b/tests/Integration/Console/ConsoleApplicationTest.php index 9daafb57c89a..bd54013c0649 100644 --- a/tests/Integration/Console/ConsoleApplicationTest.php +++ b/tests/Integration/Console/ConsoleApplicationTest.php @@ -2,20 +2,27 @@ namespace Illuminate\Tests\Integration\Console; +use Illuminate\Console\Application as Artisan; use Illuminate\Console\Command; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Contracts\Console\Kernel; use Illuminate\Foundation\Console\QueuedCommand; use Illuminate\Support\Facades\Queue; use Orchestra\Testbench\TestCase; +use Symfony\Component\Console\Attribute\AsCommand; class ConsoleApplicationTest extends TestCase { protected function setUp(): void { - parent::setUp(); + Artisan::starting(function ($artisan) { + $artisan->resolveCommands([ + FooCommandStub::class, + ZondaCommandStub::class, + ]); + }); - $this->app[Kernel::class]->registerCommand(new FooCommandStub); + parent::setUp(); } public function testArtisanCallUsingCommandName() @@ -25,6 +32,13 @@ public function testArtisanCallUsingCommandName() ])->assertExitCode(0); } + public function testArtisanCallUsingCommandNameAliases() + { + $this->artisan('app:foobar', [ + 'id' => 1, + ])->assertExitCode(0); + } + public function testArtisanCallUsingCommandClass() { $this->artisan(FooCommandStub::class, [ @@ -32,6 +46,20 @@ public function testArtisanCallUsingCommandClass() ])->assertExitCode(0); } + public function testArtisanCallUsingCommandNameUsingAsCommandAttribute() + { + $this->artisan('zonda', [ + 'id' => 1, + ])->assertExitCode(0); + } + + public function testArtisanCallUsingCommandNameAliasesUsingAsCommandAttribute() + { + $this->artisan('app:zonda', [ + 'id' => 1, + ])->assertExitCode(0); + } + public function testArtisanCallNow() { $exitCode = $this->artisan('foo:bar', [ @@ -86,6 +114,21 @@ class FooCommandStub extends Command { protected $signature = 'foo:bar {id}'; + protected $aliases = ['app:foobar']; + + public function handle() + { + // + } +} + +#[AsCommand(name: 'zonda', aliases: ['app:zonda'])] +class ZondaCommandStub extends Command +{ + protected $signature = 'zonda {id}'; + + protected $aliases = ['app:zonda']; + public function handle() { // diff --git a/tests/Integration/Console/GeneratorCommandTest.php b/tests/Integration/Console/GeneratorCommandTest.php index 18889bf5ed3b..cb8ce75a6818 100644 --- a/tests/Integration/Console/GeneratorCommandTest.php +++ b/tests/Integration/Console/GeneratorCommandTest.php @@ -3,12 +3,11 @@ namespace Illuminate\Tests\Integration\Console; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class GeneratorCommandTest extends TestCase { - /** - * @dataProvider reservedNamesDataProvider - */ + #[DataProvider('reservedNamesDataProvider')] public function testItCannotGenerateClassUsingReservedName($given) { $this->artisan('make:command', ['name' => $given]) diff --git a/tests/Integration/Console/Scheduling/CallbackEventTest.php b/tests/Integration/Console/Scheduling/CallbackEventTest.php index f884a6f3b80b..586857bd4151 100644 --- a/tests/Integration/Console/Scheduling/CallbackEventTest.php +++ b/tests/Integration/Console/Scheduling/CallbackEventTest.php @@ -10,13 +10,6 @@ class CallbackEventTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testDefaultResultIsSuccess() { $success = null; diff --git a/tests/Integration/Console/Scheduling/EventPingTest.php b/tests/Integration/Console/Scheduling/EventPingTest.php index 04c4774d3fc2..f5b648cac661 100644 --- a/tests/Integration/Console/Scheduling/EventPingTest.php +++ b/tests/Integration/Console/Scheduling/EventPingTest.php @@ -16,13 +16,6 @@ class EventPingTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - public function testPingRescuesTransferExceptions() { $this->spy(ExceptionHandler::class) diff --git a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php index bc7b98d5b1b4..de6f9cb3ae1e 100644 --- a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php +++ b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Sleep; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class SubMinuteSchedulingTest extends TestCase { @@ -45,7 +46,7 @@ public function test_it_doesnt_wait_for_sub_minute_events_when_none_are_schedule Sleep::assertNeverSlept(); } - /** @dataProvider frequencyProvider */ + #[DataProvider('frequencyProvider')] public function test_it_runs_sub_minute_callbacks($frequency, $expectedRuns) { $runs = 0; @@ -101,7 +102,7 @@ public function test_sub_minute_scheduling_can_be_interrupted() Sleep::whenFakingSleep(function ($duration) use ($startedAt) { Carbon::setTestNow(now()->add($duration)); - if (now()->diffInSeconds($startedAt) >= 30) { + if ($startedAt->diffInSeconds() >= 30) { $this->artisan('schedule:interrupt') ->expectsOutputToContain('Broadcasting schedule interrupt signal.'); } @@ -130,11 +131,11 @@ public function test_sub_minute_events_stop_for_the_rest_of_the_minute_once_main Sleep::whenFakingSleep(function ($duration) use ($startedAt) { Carbon::setTestNow(now()->add($duration)); - if (now()->diffInSeconds($startedAt) >= 30 && ! $this->app->isDownForMaintenance()) { + if ($startedAt->diffInSeconds() >= 30 && ! $this->app->isDownForMaintenance()) { $this->artisan('down'); } - if (now()->diffInSeconds($startedAt) >= 40 && $this->app->isDownForMaintenance()) { + if ($startedAt->diffInSeconds() >= 40 && $this->app->isDownForMaintenance()) { $this->artisan('up'); } }); diff --git a/tests/Integration/Cookie/CookieTest.php b/tests/Integration/Cookie/CookieTest.php index 83aa4ab9b9a4..bc5252630376 100644 --- a/tests/Integration/Cookie/CookieTest.php +++ b/tests/Integration/Cookie/CookieTest.php @@ -14,13 +14,6 @@ class CookieTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - Carbon::setTestNow(null); - } - public function test_cookie_is_sent_back_with_proper_expire_time_when_should_expire_on_close() { $this->app['config']->set('session.expire_on_close', true); diff --git a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php index bfa3d533514b..8bbdb68c0271 100644 --- a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php @@ -169,33 +169,6 @@ public function testDeviableCasts() $this->assertSame((new Decimal('320.988'))->getValue(), $model->price->getValue()); } - public function testDirtyOnCustomNumericCasts() - { - $model = new TestEloquentModelWithCustomCast; - $model->price = '123.00'; - $model->save(); - - $this->assertFalse($model->isDirty()); - - $model->price = '123.00'; - $this->assertFalse($model->isDirty('price')); - - $model->price = '123.0'; - $this->assertFalse($model->isDirty('price')); - - $model->price = '123'; - $this->assertFalse($model->isDirty('price')); - - $model->price = '00123.00'; - $this->assertFalse($model->isDirty('price')); - - $model->price = '123.4000'; - $this->assertTrue($model->isDirty('price')); - - $model->price = '123.0004'; - $this->assertTrue($model->isDirty('price')); - } - public function testSerializableCasts() { $model = new TestEloquentModelWithCustomCast; diff --git a/tests/Integration/Database/EloquentCursorPaginateTest.php b/tests/Integration/Database/EloquentCursorPaginateTest.php index 01fd4b49e8b8..54480f90bf76 100644 --- a/tests/Integration/Database/EloquentCursorPaginateTest.php +++ b/tests/Integration/Database/EloquentCursorPaginateTest.php @@ -21,6 +21,7 @@ protected function afterRefreshingDatabase() Schema::create('test_users', function ($table) { $table->increments('id'); + $table->string('name')->nullable(); $table->timestamps(); }); } @@ -167,6 +168,60 @@ public function testPaginationWithMultipleWhereClauses() ); } + public function testPaginationWithMultipleUnionAndMultipleWhereClauses() + { + TestPost::create(['title' => 'Post A', 'user_id' => 100]); + TestPost::create(['title' => 'Post B', 'user_id' => 101]); + + $table1 = TestPost::select(['id', 'title', 'user_id'])->where('user_id', 100); + $table2 = TestPost::select(['id', 'title', 'user_id'])->where('user_id', 101); + $table3 = TestPost::select(['id', 'title', 'user_id'])->where('user_id', 101); + + $columns = ['id']; + $cursorName = 'cursor-name'; + $cursor = new Cursor(['id' => 1]); + + $result = $table1->toBase() + ->union($table2->toBase()) + ->union($table3->toBase()) + ->orderBy('id', 'asc') + ->cursorPaginate(1, $columns, $cursorName, $cursor); + + $this->assertSame(['id'], $result->getOptions()['parameters']); + + $postB = $table2->where('id', '>', 1)->first(); + $this->assertEquals('Post B', $postB->title, 'Expect `Post B` is the result of the second query'); + + $this->assertCount(1, $result->items(), 'Expect cursor paginated query should have 1 result'); + $this->assertEquals('Post B', current($result->items())->title, 'Expect the paginated query would return `Post B`'); + } + + public function testPaginationWithMultipleAliases() + { + TestUser::create(['name' => 'A (user)']); + TestUser::create(['name' => 'C (user)']); + + TestPost::create(['title' => 'B (post)']); + TestPost::create(['title' => 'D (post)']); + + $table1 = TestPost::select(['title as alias']); + $table2 = TestUser::select(['name as alias']); + + $columns = ['alias']; + $cursorName = 'cursor-name'; + $cursor = new Cursor(['alias' => 'A (user)']); + + $result = $table1->toBase() + ->union($table2->toBase()) + ->orderBy('alias', 'asc') + ->cursorPaginate(1, $columns, $cursorName, $cursor); + + $this->assertSame(['alias'], $result->getOptions()['parameters']); + + $this->assertCount(1, $result->items(), 'Expect cursor paginated query should have 1 result'); + $this->assertEquals('B (post)', current($result->items())->alias, 'Expect the paginated query would return `B (post)`'); + } + public function testPaginationWithAliasedOrderBy() { for ($i = 1; $i <= 6; $i++) { diff --git a/tests/Integration/Database/EloquentModelLoadMaxTest.php b/tests/Integration/Database/EloquentModelLoadMaxTest.php new file mode 100644 index 000000000000..cf6a5d7bd8c7 --- /dev/null +++ b/tests/Integration/Database/EloquentModelLoadMaxTest.php @@ -0,0 +1,104 @@ +increments('id'); + }); + + Schema::create('related1s', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('base_model_id'); + $table->integer('number'); + }); + + Schema::create('related2s', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('base_model_id'); + $table->integer('number'); + }); + + BaseModel::create(); + + Related1::create(['base_model_id' => 1, 'number' => 10]); + Related1::create(['base_model_id' => 1, 'number' => 11]); + Related2::create(['base_model_id' => 1, 'number' => 12]); + Related2::create(['base_model_id' => 1, 'number' => 13]); + } + + public function testLoadMaxSingleRelation() + { + $model = BaseModel::first(); + + DB::enableQueryLog(); + + $model->loadMax('related1', 'number'); + + $this->assertCount(1, DB::getQueryLog()); + $this->assertEquals(11, $model->related1_max_number); + } + + public function testLoadMaxMultipleRelations() + { + $model = BaseModel::first(); + + DB::enableQueryLog(); + + $model->loadMax(['related1', 'related2'], 'number'); + + $this->assertCount(1, DB::getQueryLog()); + $this->assertEquals(11, $model->related1_max_number); + $this->assertEquals(13, $model->related2_max_number); + } +} + +class BaseModel extends Model +{ + public $timestamps = false; + + protected $guarded = []; + + public function related1() + { + return $this->hasMany(Related1::class); + } + + public function related2() + { + return $this->hasMany(Related2::class); + } +} + +class Related1 extends Model +{ + public $timestamps = false; + + protected $fillable = ['base_model_id', 'number']; + + public function parent() + { + return $this->belongsTo(BaseModel::class); + } +} + +class Related2 extends Model +{ + public $timestamps = false; + + protected $fillable = ['base_model_id', 'number']; + + public function parent() + { + return $this->belongsTo(BaseModel::class); + } +} diff --git a/tests/Integration/Database/EloquentModelLoadMinTest.php b/tests/Integration/Database/EloquentModelLoadMinTest.php new file mode 100644 index 000000000000..b63e2ec2dc47 --- /dev/null +++ b/tests/Integration/Database/EloquentModelLoadMinTest.php @@ -0,0 +1,104 @@ +increments('id'); + }); + + Schema::create('related1s', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('base_model_id'); + $table->integer('number'); + }); + + Schema::create('related2s', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('base_model_id'); + $table->integer('number'); + }); + + BaseModel::create(); + + Related1::create(['base_model_id' => 1, 'number' => 10]); + Related1::create(['base_model_id' => 1, 'number' => 11]); + Related2::create(['base_model_id' => 1, 'number' => 12]); + Related2::create(['base_model_id' => 1, 'number' => 13]); + } + + public function testLoadMinSingleRelation() + { + $model = BaseModel::first(); + + DB::enableQueryLog(); + + $model->loadMin('related1', 'number'); + + $this->assertCount(1, DB::getQueryLog()); + $this->assertEquals(10, $model->related1_min_number); + } + + public function testLoadMinMultipleRelations() + { + $model = BaseModel::first(); + + DB::enableQueryLog(); + + $model->loadMin(['related1', 'related2'], 'number'); + + $this->assertCount(1, DB::getQueryLog()); + $this->assertEquals(10, $model->related1_min_number); + $this->assertEquals(12, $model->related2_min_number); + } +} + +class BaseModel extends Model +{ + public $timestamps = false; + + protected $guarded = []; + + public function related1() + { + return $this->hasMany(Related1::class); + } + + public function related2() + { + return $this->hasMany(Related2::class); + } +} + +class Related1 extends Model +{ + public $timestamps = false; + + protected $fillable = ['base_model_id', 'number']; + + public function parent() + { + return $this->belongsTo(BaseModel::class); + } +} + +class Related2 extends Model +{ + public $timestamps = false; + + protected $fillable = ['base_model_id', 'number']; + + public function parent() + { + return $this->belongsTo(BaseModel::class); + } +} diff --git a/tests/Integration/Database/EloquentModelLoadSumTest.php b/tests/Integration/Database/EloquentModelLoadSumTest.php new file mode 100644 index 000000000000..0e4e5fa84b8d --- /dev/null +++ b/tests/Integration/Database/EloquentModelLoadSumTest.php @@ -0,0 +1,103 @@ +increments('id'); + }); + + Schema::create('related1s', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('base_model_id'); + $table->integer('number'); + }); + + Schema::create('related2s', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('base_model_id'); + $table->integer('number'); + }); + + BaseModel::create(); + + Related1::create(['base_model_id' => 1, 'number' => 10]); + Related1::create(['base_model_id' => 1, 'number' => 11]); + Related2::create(['base_model_id' => 1, 'number' => 12]); + } + + public function testLoadSumSingleRelation() + { + $model = BaseModel::first(); + + DB::enableQueryLog(); + + $model->loadSum('related1', 'number'); + + $this->assertCount(1, DB::getQueryLog()); + $this->assertEquals(21, $model->related1_sum_number); + } + + public function testLoadSumMultipleRelations() + { + $model = BaseModel::first(); + + DB::enableQueryLog(); + + $model->loadSum(['related1', 'related2'], 'number'); + + $this->assertCount(1, DB::getQueryLog()); + $this->assertEquals(21, $model->related1_sum_number); + $this->assertEquals(12, $model->related2_sum_number); + } +} + +class BaseModel extends Model +{ + public $timestamps = false; + + protected $guarded = []; + + public function related1() + { + return $this->hasMany(Related1::class); + } + + public function related2() + { + return $this->hasMany(Related2::class); + } +} + +class Related1 extends Model +{ + public $timestamps = false; + + protected $fillable = ['base_model_id', 'number']; + + public function parent() + { + return $this->belongsTo(BaseModel::class); + } +} + +class Related2 extends Model +{ + public $timestamps = false; + + protected $fillable = ['base_model_id', 'number']; + + public function parent() + { + return $this->belongsTo(BaseModel::class); + } +} diff --git a/tests/Integration/Database/EloquentMorphEagerLoadingTest.php b/tests/Integration/Database/EloquentMorphEagerLoadingTest.php index c251f0c105c7..8f6d4405bd63 100644 --- a/tests/Integration/Database/EloquentMorphEagerLoadingTest.php +++ b/tests/Integration/Database/EloquentMorphEagerLoadingTest.php @@ -95,6 +95,21 @@ public function testMorphLoadingMixedWithTrashedRelations() $this->assertTrue($action[1]->relationLoaded('target')); $this->assertInstanceOf(User::class, $action[1]->getRelation('target')); } + + public function testMorphWithTrashedRelationLazyLoading() + { + $deletedUser = User::forceCreate(['deleted_at' => now()]); + + $action = new Action; + $action->target()->associate($deletedUser)->save(); + + // model is already set via associate and not retrieved from the database + $this->assertInstanceOf(User::class, $action->target); + + $action->unsetRelation('target'); + + $this->assertInstanceOf(User::class, $action->target); + } } class Action extends Model diff --git a/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php b/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php index 262ef43d9c5d..e353d0ef099c 100644 --- a/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php +++ b/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php @@ -101,6 +101,45 @@ public function testObserverIsCalledEvenWhenDeeplyNestingTransactions() $this->assertTrue($user1->exists); $this->assertEquals(1, $observer::$calledTimes, 'Failed to assert the observer was called once.'); } + + public function testTransactionCallbackExceptions() + { + [$firstObject, $secondObject] = [ + new EloquentTransactionWithAfterCommitTestsTestObjectForTransactions(), + new EloquentTransactionWithAfterCommitTestsTestObjectForTransactions(), + ]; + + $rootTransactionLevel = DB::transactionLevel(); + + // After commit callbacks may fail with an exception. When they do, the rest of the callbacks are not + // executed. It's important that the transaction would already be committed by that point, so the + // transaction level should be modified before executing any callbacks. Also, exceptions in the + // callbacks should not affect the connection's transaction level. + $this->assertThrows(function () use ($rootTransactionLevel, $secondObject, $firstObject) { + DB::transaction(function () use ($rootTransactionLevel, $firstObject, $secondObject) { + DB::transaction(function () use ($rootTransactionLevel, $firstObject) { + $this->assertSame($rootTransactionLevel + 2, DB::transactionLevel()); + + DB::afterCommit(function () use ($rootTransactionLevel, $firstObject) { + $this->assertSame($rootTransactionLevel, DB::transactionLevel()); + + $firstObject->handle(); + }); + }); + + $this->assertSame($rootTransactionLevel + 1, DB::transactionLevel()); + + DB::afterCommit(fn () => throw new \RuntimeException()); + DB::afterCommit(fn () => $secondObject->handle()); + }); + }, \RuntimeException::class); + + $this->assertSame($rootTransactionLevel, DB::transactionLevel()); + + $this->assertTrue($firstObject->ran); + $this->assertFalse($secondObject->ran); + $this->assertEquals(1, $firstObject->runs); + } } class EloquentTransactionWithAfterCommitTestsUserObserver @@ -150,3 +189,16 @@ public function handle(): void }); } } + +class EloquentTransactionWithAfterCommitTestsTestObjectForTransactions +{ + public $ran = false; + + public $runs = 0; + + public function handle() + { + $this->ran = true; + $this->runs++; + } +} diff --git a/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php b/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php index 16b067eac3ca..4f5df6d24f74 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Database\MySql; +use Illuminate\Database\Events\QueryExecuted; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -151,4 +152,27 @@ public static function jsonContainsKeyDataProvider() 'null value' => [1, 'json_col->bar'], ]; } + + public function testLastInsertIdIsPreserved() + { + if (! Schema::hasTable('auto_id_table')) { + Schema::create('auto_id_table', function (Blueprint $table) { + $table->id(); + }); + } + + try { + static $callbackExecuted = false; + DB::listen(function (QueryExecuted $event) use (&$callbackExecuted) { + DB::getPdo()->query('SELECT 1'); + $callbackExecuted = true; + }); + + $id = DB::table('auto_id_table')->insertGetId([]); + $this->assertTrue($callbackExecuted, 'The query listener was not executed.'); + $this->assertEquals(1, $id); + } finally { + Schema::drop('auto_id_table'); + } + } } diff --git a/tests/Integration/Database/MySql/JoinLateralTest.php b/tests/Integration/Database/MySql/JoinLateralTest.php new file mode 100644 index 000000000000..969308ff856c --- /dev/null +++ b/tests/Integration/Database/MySql/JoinLateralTest.php @@ -0,0 +1,117 @@ +id('id'); + $table->string('name'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id('id'); + $table->string('title'); + $table->integer('rating'); + $table->unsignedBigInteger('user_id'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('posts'); + Schema::drop('users'); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->checkMySqlVersion(); + + DB::table('users')->insert([ + ['name' => Str::random()], + ['name' => Str::random()], + ]); + + DB::table('posts')->insert([ + ['title' => Str::random(), 'rating' => 1, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 3, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 7, 'user_id' => 1], + ]); + } + + protected function checkMySqlVersion() + { + $mySqlVersion = DB::select('select version()')[0]->{'version()'} ?? ''; + + if (strpos($mySqlVersion, 'Maria') !== false) { + $this->markTestSkipped('Lateral joins are not supported on MariaDB'.__CLASS__); + } elseif ((float) $mySqlVersion < '8.0.14') { + $this->markTestSkipped('Lateral joins are not supported on MySQL < 8.0.14'.__CLASS__); + } + } + + public function testJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(0, $userWithoutPosts); + } + + public function testLeftJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(1, $userWithoutPosts); + $this->assertNull($userWithoutPosts[0]->best_post_title); + $this->assertNull($userWithoutPosts[0]->best_post_rating); + } +} diff --git a/tests/Integration/Database/Postgres/JoinLateralTest.php b/tests/Integration/Database/Postgres/JoinLateralTest.php new file mode 100644 index 000000000000..e17f5622efdd --- /dev/null +++ b/tests/Integration/Database/Postgres/JoinLateralTest.php @@ -0,0 +1,104 @@ +id('id'); + $table->string('name'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id('id'); + $table->string('title'); + $table->integer('rating'); + $table->unsignedBigInteger('user_id'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('posts'); + Schema::drop('users'); + } + + protected function setUp(): void + { + parent::setUp(); + + DB::table('users')->insert([ + ['name' => Str::random()], + ['name' => Str::random()], + ]); + + DB::table('posts')->insert([ + ['title' => Str::random(), 'rating' => 1, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 3, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 7, 'user_id' => 1], + ]); + } + + public function testJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(0, $userWithoutPosts); + } + + public function testLeftJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(1, $userWithoutPosts); + $this->assertNull($userWithoutPosts[0]->best_post_title); + $this->assertNull($userWithoutPosts[0]->best_post_rating); + } +} diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index fbd42e13c489..6fd62e399cfc 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -193,15 +193,20 @@ public function testGetAndDropTypes() DB::statement("create type enum_foo as enum ('new', 'open', 'closed')"); DB::statement('create type range_foo as range (subtype = float8)'); DB::statement('create domain domain_foo as text'); + DB::statement('create type base_foo'); + DB::statement("create function foo_in(cstring) returns base_foo language internal immutable strict parallel safe as 'int2in'"); + DB::statement("create function foo_out(base_foo) returns cstring language internal immutable strict parallel safe as 'int2out'"); + DB::statement('create type base_foo (input = foo_in, output = foo_out)'); $types = Schema::getTypes(); - $this->assertCount(11, $types); + $this->assertCount(13, $types); $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'pseudo_foo' && $type['type'] === 'pseudo' && ! $type['implicit'])); $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'comp_foo' && $type['type'] === 'composite' && ! $type['implicit'])); $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'enum_foo' && $type['type'] === 'enum' && ! $type['implicit'])); $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'range_foo' && $type['type'] === 'range' && ! $type['implicit'])); $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'domain_foo' && $type['type'] === 'domain' && ! $type['implicit'])); + $this->assertTrue(collect($types)->contains(fn ($type) => $type['name'] === 'base_foo' && $type['type'] === 'base' && ! $type['implicit'])); Schema::dropAllTypes(); $types = Schema::getTypes(); @@ -256,6 +261,10 @@ public function testGetIndexes() && ! $indexes[0]['unique'] && ! $indexes[0]['primary'] ); + $this->assertTrue(Schema::hasIndex('foo', 'my_index')); + $this->assertTrue(Schema::hasIndex('foo', ['bar'])); + $this->assertFalse(Schema::hasIndex('foo', 'my_index', 'primary')); + $this->assertFalse(Schema::hasIndex('foo', ['bar'], 'unique')); } public function testGetUniqueIndexes() @@ -277,6 +286,11 @@ public function testGetUniqueIndexes() $this->assertTrue(collect($indexes)->contains( fn ($index) => $index['name'] === 'foo_baz_bar_unique' && $index['columns'] === ['baz', 'bar'] && $index['unique'] )); + $this->assertTrue(Schema::hasIndex('foo', 'foo_baz_bar_unique')); + $this->assertTrue(Schema::hasIndex('foo', 'foo_baz_bar_unique', 'unique')); + $this->assertTrue(Schema::hasIndex('foo', ['baz', 'bar'])); + $this->assertTrue(Schema::hasIndex('foo', ['baz', 'bar'], 'unique')); + $this->assertFalse(Schema::hasIndex('foo', ['baz', 'bar'], 'primary')); } public function testGetIndexesWithCompositeKeys() @@ -321,6 +335,26 @@ public function testGetFullTextIndexes() $this->assertTrue(collect($indexes)->contains('name', 'articles_body_title_fulltext')); } + public function testHasIndexOrder() + { + Schema::create('foo', function (Blueprint $table) { + $table->integer('bar'); + $table->integer('baz'); + $table->integer('qux'); + + $table->unique(['bar', 'baz']); + $table->index(['baz', 'bar']); + $table->index(['baz', 'qux']); + }); + + $this->assertTrue(Schema::hasIndex('foo', ['bar', 'baz'])); + $this->assertTrue(Schema::hasIndex('foo', ['bar', 'baz'], 'unique')); + $this->assertTrue(Schema::hasIndex('foo', ['baz', 'bar'])); + $this->assertFalse(Schema::hasIndex('foo', ['baz', 'bar'], 'unique')); + $this->assertTrue(Schema::hasIndex('foo', ['baz', 'qux'])); + $this->assertFalse(Schema::hasIndex('foo', ['qux', 'baz'])); + } + public function testGetForeignKeys() { Schema::create('users', function (Blueprint $table) { diff --git a/tests/Integration/Database/SqlServer/JoinLateralTest.php b/tests/Integration/Database/SqlServer/JoinLateralTest.php new file mode 100644 index 000000000000..df11c5517585 --- /dev/null +++ b/tests/Integration/Database/SqlServer/JoinLateralTest.php @@ -0,0 +1,100 @@ +id('id'); + $table->string('name'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id('id'); + $table->string('title'); + $table->integer('rating'); + $table->unsignedBigInteger('user_id'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('posts'); + Schema::drop('users'); + } + + protected function setUp(): void + { + parent::setUp(); + + DB::table('users')->insert([ + ['name' => Str::random()], + ['name' => Str::random()], + ]); + + DB::table('posts')->insert([ + ['title' => Str::random(), 'rating' => 1, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 3, 'user_id' => 1], + ['title' => Str::random(), 'rating' => 7, 'user_id' => 1], + ]); + } + + public function testJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, (int) $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, (int) $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->joinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(0, $userWithoutPosts); + } + + public function testLeftJoinLateral() + { + $subquery = DB::table('posts') + ->select('title as best_post_title', 'rating as best_post_rating') + ->whereColumn('user_id', 'users.id') + ->orderBy('rating', 'desc') + ->limit(2); + + $userWithPosts = DB::table('users') + ->where('id', 1) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(2, $userWithPosts); + $this->assertEquals(7, (int) $userWithPosts[0]->best_post_rating); + $this->assertEquals(3, (int) $userWithPosts[1]->best_post_rating); + + $userWithoutPosts = DB::table('users') + ->where('id', 2) + ->leftJoinLateral($subquery, 'best_post') + ->get(); + + $this->assertCount(1, $userWithoutPosts); + $this->assertNull($userWithoutPosts[0]->best_post_title); + $this->assertNull($userWithoutPosts[0]->best_post_rating); + } +} diff --git a/tests/Integration/Encryption/EncryptionTest.php b/tests/Integration/Encryption/EncryptionTest.php index 1ef8dab14193..d0a281bc292c 100644 --- a/tests/Integration/Encryption/EncryptionTest.php +++ b/tests/Integration/Encryption/EncryptionTest.php @@ -3,22 +3,13 @@ namespace Illuminate\Tests\Integration\Encryption; use Illuminate\Encryption\Encrypter; -use Illuminate\Encryption\EncryptionServiceProvider; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; use RuntimeException; +#[WithConfig('app.key', 'base64:IUHRqAQ99pZ0A1MPjbuv1D6ff3jxv0GIvS2qIW4JNU4=')] class EncryptionTest extends TestCase { - protected function getEnvironmentSetUp($app) - { - $app['config']->set('app.key', 'base64:IUHRqAQ99pZ0A1MPjbuv1D6ff3jxv0GIvS2qIW4JNU4='); - } - - protected function getPackageProviders($app) - { - return [EncryptionServiceProvider::class]; - } - public function testEncryptionProviderBind() { $this->assertInstanceOf(Encrypter::class, $this->app->make('encrypter')); diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php index a7e9b97cf096..b6693eed94bc 100644 --- a/tests/Integration/Events/EventFakeTest.php +++ b/tests/Integration/Events/EventFakeTest.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; @@ -16,10 +17,10 @@ class EventFakeTest extends TestCase { - protected function setUp(): void - { - parent::setUp(); + use LazilyRefreshDatabase; + protected function afterRefreshingDatabase() + { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('title'); @@ -28,11 +29,9 @@ protected function setUp(): void }); } - protected function tearDown(): void + protected function beforeRefreshingDatabase() { Schema::dropIfExists('posts'); - - parent::tearDown(); } public function testNonFakedEventGetsProperlyDispatched() diff --git a/tests/Integration/Events/ListenerTest.php b/tests/Integration/Events/ListenerTest.php index 76cdcb55c691..490117eef174 100644 --- a/tests/Integration/Events/ListenerTest.php +++ b/tests/Integration/Events/ListenerTest.php @@ -14,7 +14,7 @@ protected function tearDown(): void ListenerTestListener::$ran = false; ListenerTestListenerAfterCommit::$ran = false; - m::close(); + parent::tearDown(); } public function testClassListenerRunsNormallyIfNoTransactions() diff --git a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php index 6b11d4f9916e..5ec65c1ed338 100644 --- a/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php +++ b/tests/Integration/Events/ShouldDispatchAfterCommitEventTest.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; -use Mockery as m; use Orchestra\Testbench\TestCase; class ShouldDispatchAfterCommitEventTest extends TestCase @@ -16,7 +15,7 @@ protected function tearDown(): void ShouldDispatchAfterCommitTestEvent::$ran = false; AnotherShouldDispatchAfterCommitTestEvent::$ran = false; - m::close(); + parent::tearDown(); } public function testEventIsDispatchedIfThereIsNoTransaction() diff --git a/tests/Integration/Filesystem/FilesystemTest.php b/tests/Integration/Filesystem/FilesystemTest.php index 8bd38fe309fe..50d8965c82c6 100644 --- a/tests/Integration/Filesystem/FilesystemTest.php +++ b/tests/Integration/Filesystem/FilesystemTest.php @@ -4,11 +4,10 @@ use Illuminate\Support\Facades\File; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use Symfony\Component\Process\Process; -/** - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|DAR')] class FilesystemTest extends TestCase { protected $stubFile; diff --git a/tests/Integration/Filesystem/StorageTest.php b/tests/Integration/Filesystem/StorageTest.php index 4193a629e178..90ba8fbfe58e 100644 --- a/tests/Integration/Filesystem/StorageTest.php +++ b/tests/Integration/Filesystem/StorageTest.php @@ -5,11 +5,10 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use Symfony\Component\Process\Process; -/** - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|DAR')] class StorageTest extends TestCase { protected $stubFile; diff --git a/tests/Integration/Foundation/Console/AboutCommandTest.php b/tests/Integration/Foundation/Console/AboutCommandTest.php index 6795d3bafeb2..d671ba8207c4 100644 --- a/tests/Integration/Foundation/Console/AboutCommandTest.php +++ b/tests/Integration/Foundation/Console/AboutCommandTest.php @@ -11,13 +11,13 @@ class AboutCommandTest extends TestCase { public function testItCanDisplayAboutCommandAsJson() { - $process = remote('about --json')->mustRun(); + $process = remote('about --json', ['APP_ENV' => 'local'])->mustRun(); tap(json_decode($process->getOutput(), true), function ($output) { Assert::assertArraySubset([ 'application_name' => 'Laravel', 'php_version' => PHP_VERSION, - 'environment' => 'testing', + 'environment' => 'local', 'debug_mode' => true, 'url' => 'localhost', 'maintenance_mode' => false, diff --git a/tests/Integration/Foundation/MaintenanceModeTest.php b/tests/Integration/Foundation/MaintenanceModeTest.php index 493934b6dd4a..377bff02858f 100644 --- a/tests/Integration/Foundation/MaintenanceModeTest.php +++ b/tests/Integration/Foundation/MaintenanceModeTest.php @@ -19,16 +19,15 @@ class MaintenanceModeTest extends TestCase { protected function setUp(): void { + $this->beforeApplicationDestroyed(function () { + @unlink(storage_path('framework/down')); + }); + parent::setUp(); $this->withoutMiddleware(TestbenchPreventRequestsDuringMaintenance::class); } - protected function tearDown(): void - { - @unlink(storage_path('framework/down')); - } - public function testBasicMaintenanceModeResponse() { file_put_contents(storage_path('framework/down'), json_encode([ @@ -172,8 +171,6 @@ public function testCanCreateBypassCookies() Carbon::setTestNow(now()->addMonths(6)); $this->assertFalse(MaintenanceModeBypassCookie::isValid($cookie->getValue(), 'test-key')); - - Carbon::setTestNow(null); } public function testDispatchEventWhenMaintenanceModeIsEnabled() diff --git a/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php b/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php index 01e97f5b0cd4..2ccaa6775b94 100644 --- a/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php +++ b/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php @@ -3,19 +3,22 @@ namespace Illuminate\Tests\Integration\Foundation\Testing\Concerns; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Foundation\Auth\User; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; +#[WithMigration] class InteractsWithAuthenticationTest extends TestCase { - protected function getEnvironmentSetUp($app) - { - $app['config']->set('auth.providers.users.model', AuthenticationTestUser::class); + use RefreshDatabase; + protected function defineEnvironment($app) + { $app['config']->set('auth.guards.api', [ 'driver' => 'token', 'provider' => 'users', @@ -23,20 +26,17 @@ protected function getEnvironmentSetUp($app) ]); } - protected function setUp(): void + protected function afterRefreshingDatabase() { - parent::setUp(); + Schema::table('users', function (Blueprint $table) { + $table->renameColumn('name', 'username'); + }); - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('email'); - $table->string('username'); - $table->string('password'); - $table->string('remember_token')->default(null)->nullable(); + Schema::table('users', function (Blueprint $table) { $table->tinyInteger('is_active')->default(0); }); - AuthenticationTestUser::create([ + User::forceCreate([ 'username' => 'taylorotwell', 'email' => 'taylorotwell@laravel.com', 'password' => bcrypt('password'), @@ -50,7 +50,7 @@ public function testActingAsIsProperlyHandledForSessionAuth() return 'Hello '.$request->user()->username; })->middleware(['auth']); - $user = AuthenticationTestUser::where('username', '=', 'taylorotwell')->first(); + $user = User::where('username', '=', 'taylorotwell')->first(); $this->actingAs($user) ->get('/me') @@ -68,7 +68,7 @@ public function testActingAsIsProperlyHandledForAuthViaRequest() return $request->user(); }); - $user = AuthenticationTestUser::where('username', '=', 'taylorotwell')->first(); + $user = User::where('username', '=', 'taylorotwell')->first(); $this->actingAs($user, 'api') ->get('/me') @@ -76,25 +76,3 @@ public function testActingAsIsProperlyHandledForAuthViaRequest() ->assertSeeText('Hello taylorotwell'); } } - -class AuthenticationTestUser extends Authenticatable -{ - public $table = 'users'; - public $timestamps = false; - - /** - * The attributes that are mass assignable. - * - * @var string[] - */ - protected $guarded = []; - - /** - * The attributes that should be hidden for arrays. - * - * @var string[] - */ - protected $hidden = [ - 'password', 'remember_token', - ]; -} diff --git a/tests/Integration/Http/Middleware/HandleCorsTest.php b/tests/Integration/Http/Middleware/HandleCorsTest.php index e30eaf8324f3..38ed647a621a 100644 --- a/tests/Integration/Http/Middleware/HandleCorsTest.php +++ b/tests/Integration/Http/Middleware/HandleCorsTest.php @@ -13,23 +13,8 @@ class HandleCorsTest extends TestCase { use ValidatesRequests; - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { - $kernel = $app->make(Kernel::class); - $kernel->prependMiddleware(HandleCors::class); - - $router = $app['router']; - - $this->addWebRoutes($router); - $this->addApiRoutes($router); - - parent::getEnvironmentSetUp($app); - } - - protected function resolveApplicationConfiguration($app) - { - parent::resolveApplicationConfiguration($app); - $app['config']['cors'] = [ 'paths' => ['api/*'], 'supports_credentials' => false, @@ -39,6 +24,15 @@ protected function resolveApplicationConfiguration($app) 'exposed_headers' => [], 'max_age' => 0, ]; + + $kernel = $app->make(Kernel::class); + $kernel->prependMiddleware(HandleCors::class); + } + + protected function defineRoutes($router) + { + $this->addWebRoutes($router); + $this->addApiRoutes($router); } public function testShouldReturnHeaderAssessControlAllowOriginWhenDontHaveHttpOriginOnRequest() diff --git a/tests/Integration/Http/ThrottleRequestsTest.php b/tests/Integration/Http/ThrottleRequestsTest.php index 6ddd2a0ef51e..d3cb97cd3d20 100644 --- a/tests/Integration/Http/ThrottleRequestsTest.php +++ b/tests/Integration/Http/ThrottleRequestsTest.php @@ -9,23 +9,13 @@ use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Route; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; use Throwable; +#[WithConfig('hashing.driver', 'bcrypt')] class ThrottleRequestsTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - - Carbon::setTestNow(null); - } - - public function getEnvironmentSetUp($app) - { - $app['config']->set('hashing', ['driver' => 'bcrypt']); - } - public function testLockOpensImmediatelyAfterDecay() { Carbon::setTestNow(Carbon::create(2018, 1, 1, 0, 0, 0)); diff --git a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php index ef7a98b75401..37f98dc73f33 100644 --- a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php +++ b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php @@ -6,24 +6,15 @@ use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Route; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; use Throwable; +#[WithConfig('hashing.driver', 'bcrypt')] class ThrottleRequestsWithRedisTest extends TestCase { use InteractsWithRedis; - protected function tearDown(): void - { - parent::tearDown(); - Carbon::setTestNow(null); - } - - public function getEnvironmentSetUp($app) - { - $app['config']->set('hashing', ['driver' => 'bcrypt']); - } - public function testLockOpensImmediatelyAfterDecay() { $this->ifRedisAvailable(function () { diff --git a/tests/Integration/Mail/RenderingMailWithLocaleTest.php b/tests/Integration/Mail/RenderingMailWithLocaleTest.php index 2780d60b5568..b3cd5e995657 100644 --- a/tests/Integration/Mail/RenderingMailWithLocaleTest.php +++ b/tests/Integration/Mail/RenderingMailWithLocaleTest.php @@ -3,18 +3,17 @@ namespace Illuminate\Tests\Integration\Mail; use Illuminate\Mail\Mailable; -use Illuminate\Support\Facades\View; use Orchestra\Testbench\TestCase; class RenderingMailWithLocaleTest extends TestCase { - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $app['config']->set('app.locale', 'en'); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addLocation(__DIR__.'/Fixtures'); - app('translator')->setLoaded([ + $app['translator']->setLoaded([ '*' => [ '*' => [ 'en' => ['nom' => 'name'], diff --git a/tests/Integration/Mail/SendingMailWithLocaleTest.php b/tests/Integration/Mail/SendingMailWithLocaleTest.php index 8b307c058188..06a6dc096e45 100644 --- a/tests/Integration/Mail/SendingMailWithLocaleTest.php +++ b/tests/Integration/Mail/SendingMailWithLocaleTest.php @@ -9,21 +9,20 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Facades\View; use Illuminate\Testing\Assert; use Orchestra\Testbench\TestCase; class SendingMailWithLocaleTest extends TestCase { - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $app['config']->set('mail.driver', 'array'); $app['config']->set('app.locale', 'en'); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addLocation(__DIR__.'/Fixtures'); - app('translator')->setLoaded([ + $app['translator']->setLoaded([ '*' => [ '*' => [ 'en' => ['nom' => 'name'], diff --git a/tests/Integration/Mail/SendingMarkdownMailTest.php b/tests/Integration/Mail/SendingMarkdownMailTest.php index b3e5a561b85d..69bb8bb4d529 100644 --- a/tests/Integration/Mail/SendingMarkdownMailTest.php +++ b/tests/Integration/Mail/SendingMarkdownMailTest.php @@ -7,7 +7,6 @@ use Illuminate\Mail\Mailables\Envelope; use Illuminate\Mail\Markdown; use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Facades\View; use Orchestra\Testbench\TestCase; class SendingMarkdownMailTest extends TestCase @@ -16,8 +15,8 @@ protected function getEnvironmentSetUp($app) { $app['config']->set('mail.driver', 'array'); - View::addNamespace('mail', __DIR__.'/Fixtures'); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addNamespace('mail', __DIR__.'/Fixtures') + ->addLocation(__DIR__.'/Fixtures'); } public function testMailIsSent() diff --git a/tests/Integration/Mail/SendingQueuedMailTest.php b/tests/Integration/Mail/SendingQueuedMailTest.php index 6f5cc503ee95..6f982043ce65 100644 --- a/tests/Integration/Mail/SendingQueuedMailTest.php +++ b/tests/Integration/Mail/SendingQueuedMailTest.php @@ -7,7 +7,6 @@ use Illuminate\Queue\Middleware\RateLimited; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Queue; -use Illuminate\Support\Facades\View; use Orchestra\Testbench\TestCase; class SendingQueuedMailTest extends TestCase @@ -16,7 +15,7 @@ protected function getEnvironmentSetUp($app) { $app['config']->set('mail.driver', 'array'); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addLocation(__DIR__.'/Fixtures'); } public function testMailIsSentWithDefaultLocale() diff --git a/tests/Integration/Mail/SentMessageMailTest.php b/tests/Integration/Mail/SentMessageMailTest.php index af72a6c33159..76350090e3fd 100644 --- a/tests/Integration/Mail/SentMessageMailTest.php +++ b/tests/Integration/Mail/SentMessageMailTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Notifications\Events\NotificationSent; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; @@ -14,15 +15,20 @@ class SentMessageMailTest extends TestCase { - protected function setUp(): void - { - parent::setUp(); + use LazilyRefreshDatabase; + protected function afterRefreshingDatabase() + { Schema::create('sent_message_users', function (Blueprint $table) { $table->increments('id'); }); } + protected function beforeRefreshingDatabase() + { + Schema::dropIfExists('sent_message_users'); + } + public function testDispatchesNotificationSent() { $notificationWasSent = false; diff --git a/tests/Integration/Notifications/SendingMailNotificationsTest.php b/tests/Integration/Notifications/SendingMailNotificationsTest.php index 26cf3e4e391b..9b59f4c7c0da 100644 --- a/tests/Integration/Notifications/SendingMailNotificationsTest.php +++ b/tests/Integration/Notifications/SendingMailNotificationsTest.php @@ -14,7 +14,6 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Schema; -use Illuminate\Support\Facades\View; use Illuminate\Support\Str; use Mockery as m; use Orchestra\Testbench\TestCase; @@ -25,14 +24,7 @@ class SendingMailNotificationsTest extends TestCase public $mailer; public $markdown; - protected function tearDown(): void - { - parent::tearDown(); - - m::close(); - } - - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $this->mailFactory = m::mock(MailFactory::class); $this->mailer = m::mock(Mailer::class); @@ -51,7 +43,7 @@ protected function getEnvironmentSetUp($app) return $this->mailFactory; }); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addLocation(__DIR__.'/Fixtures'); } protected function setUp(): void diff --git a/tests/Integration/Notifications/SendingMailableNotificationsTest.php b/tests/Integration/Notifications/SendingMailableNotificationsTest.php index c074e4490974..dcbecf0c4fb1 100644 --- a/tests/Integration/Notifications/SendingMailableNotificationsTest.php +++ b/tests/Integration/Notifications/SendingMailableNotificationsTest.php @@ -4,18 +4,18 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Schema; -use Illuminate\Support\Facades\View; use Orchestra\Testbench\TestCase; class SendingMailableNotificationsTest extends TestCase { - public $mailer; + use RefreshDatabase; - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $app['config']->set('mail.driver', 'array'); @@ -23,13 +23,11 @@ protected function getEnvironmentSetUp($app) $app['config']->set('mail.markdown.theme', 'blank'); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addLocation(__DIR__.'/Fixtures'); } - protected function setUp(): void + protected function afterRefreshingDatabase() { - parent::setUp(); - Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email'); @@ -37,6 +35,11 @@ protected function setUp(): void }); } + protected function beforeRefreshingDatabase() + { + Schema::dropIfExists('users'); + } + public function testMarkdownNotification() { $user = MailableNotificationUser::forceCreate([ diff --git a/tests/Integration/Notifications/SendingNotificationsViaAnonymousNotifiableTest.php b/tests/Integration/Notifications/SendingNotificationsViaAnonymousNotifiableTest.php index d48941336c86..4ba3ea787a54 100644 --- a/tests/Integration/Notifications/SendingNotificationsViaAnonymousNotifiableTest.php +++ b/tests/Integration/Notifications/SendingNotificationsViaAnonymousNotifiableTest.php @@ -10,8 +10,6 @@ class SendingNotificationsViaAnonymousNotifiableTest extends TestCase { - public $mailer; - public function testMailIsSent() { $notifiable = (new AnonymousNotifiable) @@ -28,6 +26,20 @@ public function testMailIsSent() ], $_SERVER['__notifiable.route']); } + public function testAnonymousNotifiableWithMultipleRoutes() + { + $_SERVER['__notifiable.route'] = []; + + NotificationFacade::routes([ + 'testchannel' => 'enzo', + 'anothertestchannel' => 'enzo@deepblue.com', + ])->notify(new TestMailNotificationForAnonymousNotifiable()); + + $this->assertEquals([ + 'enzo', 'enzo@deepblue.com', + ], $_SERVER['__notifiable.route']); + } + public function testFaking() { $fake = NotificationFacade::fake(); diff --git a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php index 9b9862abfa51..95a0f4e39f38 100644 --- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php +++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php @@ -15,23 +15,20 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Notification as NotificationFacade; use Illuminate\Support\Facades\Schema; -use Illuminate\Support\Facades\View; use Illuminate\Testing\Assert; use Orchestra\Testbench\TestCase; class SendingNotificationsWithLocaleTest extends TestCase { - public $mailer; - - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $app['config']->set('mail.driver', 'array'); $app['config']->set('app.locale', 'en'); - View::addLocation(__DIR__.'/Fixtures'); + $app['view']->addLocation(__DIR__.'/Fixtures'); - app('translator')->setLoaded([ + $app['translator']->setLoaded([ '*' => [ '*' => [ 'en' => ['hi' => 'hello'], diff --git a/tests/Integration/Queue/RedisQueueTest.php b/tests/Integration/Queue/RedisQueueTest.php index 74f1dffcec4a..0905ada8d4c3 100644 --- a/tests/Integration/Queue/RedisQueueTest.php +++ b/tests/Integration/Queue/RedisQueueTest.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Queue\Events\JobQueued; +use Illuminate\Queue\Events\JobQueueing; use Illuminate\Queue\Jobs\RedisJob; use Illuminate\Queue\RedisQueue; use Illuminate\Support\InteractsWithTime; @@ -200,7 +201,7 @@ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) $job = new RedisQueueIntegrationTestJob(10); $this->queue->later(-10, $job); - $this->container->shouldHaveReceived('bound')->with('events')->once(); + $this->container->shouldHaveReceived('bound')->with('events')->twice(); // Pop and check it is popped correctly $before = $this->currentTime(); @@ -278,7 +279,7 @@ public function testNotExpireJobsWhenExpireNull($driver) // Make an expired reserved job $failed = new RedisQueueIntegrationTestJob(-20); $this->queue->push($failed); - $this->container->shouldHaveReceived('bound')->with('events')->once(); + $this->container->shouldHaveReceived('bound')->with('events')->twice(); $beforeFailPop = $this->currentTime(); $this->queue->pop(); @@ -287,7 +288,7 @@ public function testNotExpireJobsWhenExpireNull($driver) // Push an item into queue $job = new RedisQueueIntegrationTestJob(10); $this->queue->push($job); - $this->container->shouldHaveReceived('bound')->with('events')->times(2); + $this->container->shouldHaveReceived('bound')->with('events')->times(4); // Pop and check it is popped correctly $before = $this->currentTime(); @@ -326,7 +327,7 @@ public function testExpireJobsWhenExpireSet($driver) // Push an item into queue $job = new RedisQueueIntegrationTestJob(10); $this->queue->push($job); - $this->container->shouldHaveReceived('bound')->with('events')->once(); + $this->container->shouldHaveReceived('bound')->with('events')->twice(); // Pop and check it is popped correctly $before = $this->currentTime(); @@ -466,19 +467,24 @@ public function testSize($driver) * @param string $driver */ #[DataProvider('redisDriverProvider')] - public function testPushJobQueuedEvent($driver) + public function testPushJobQueueingAndJobQueuedEvents($driver) { $events = m::mock(Dispatcher::class); + $events->shouldReceive('dispatch')->withArgs(function (JobQueueing $jobQueuing) { + $this->assertInstanceOf(RedisQueueIntegrationTestJob::class, $jobQueuing->job); + + return true; + })->andReturnNull()->once(); $events->shouldReceive('dispatch')->withArgs(function (JobQueued $jobQueued) { $this->assertInstanceOf(RedisQueueIntegrationTestJob::class, $jobQueued->job); - $this->assertIsString(RedisQueueIntegrationTestJob::class, $jobQueued->id); + $this->assertIsString($jobQueued->id); return true; })->andReturnNull()->once(); $container = m::mock(Container::class); - $container->shouldReceive('bound')->with('events')->andReturn(true)->once(); - $container->shouldReceive('offsetGet')->with('events')->andReturn($events)->once(); + $container->shouldReceive('bound')->with('events')->andReturn(true)->twice(); + $container->shouldReceive('offsetGet')->with('events')->andReturn($events)->twice(); $queue = new RedisQueue($this->redis[$driver]); $queue->setContainer($container); @@ -493,11 +499,12 @@ public function testPushJobQueuedEvent($driver) public function testBulkJobQueuedEvent($driver) { $events = m::mock(Dispatcher::class); + $events->shouldReceive('dispatch')->with(m::type(JobQueueing::class))->andReturnNull()->times(3); $events->shouldReceive('dispatch')->with(m::type(JobQueued::class))->andReturnNull()->times(3); $container = m::mock(Container::class); - $container->shouldReceive('bound')->with('events')->andReturn(true)->times(3); - $container->shouldReceive('offsetGet')->with('events')->andReturn($events)->times(3); + $container->shouldReceive('bound')->with('events')->andReturn(true)->times(6); + $container->shouldReceive('offsetGet')->with('events')->andReturn($events)->times(6); $queue = new RedisQueue($this->redis[$driver]); $queue->setContainer($container); diff --git a/tests/Integration/Redis/PredisConnectionTest.php b/tests/Integration/Redis/PredisConnectionTest.php index e25dbbc8fe4e..2ec51d21521a 100644 --- a/tests/Integration/Redis/PredisConnectionTest.php +++ b/tests/Integration/Redis/PredisConnectionTest.php @@ -6,17 +6,14 @@ use Illuminate\Redis\Events\CommandExecuted; use Illuminate\Support\Facades\Event; use Mockery as m; +use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; use Predis\Client; use Predis\Command\Argument\Search\SearchArguments; +#[WithConfig('database.redis.client', 'predis')] class PredisConnectionTest extends TestCase { - protected function defineEnvironment($app) - { - $app->get('config')->set('database.redis.client', 'predis'); - } - public function testPredisCanEmitEventWithArrayableArgumentObject() { if (! class_exists(SearchArguments::class)) { diff --git a/tests/Integration/Routing/CategoryBackedEnum.php b/tests/Integration/Routing/CategoryBackedEnum.php new file mode 100644 index 000000000000..2ff3cf6e7f80 --- /dev/null +++ b/tests/Integration/Routing/CategoryBackedEnum.php @@ -0,0 +1,18 @@ + self::People, + 'c02' => self::Fruits, + default => null, + }; + } +} diff --git a/tests/Integration/Routing/Enums.php b/tests/Integration/Routing/Enums.php deleted file mode 100644 index 5a2ba0f5a8b1..000000000000 --- a/tests/Integration/Routing/Enums.php +++ /dev/null @@ -1,9 +0,0 @@ -value; })->middleware('web'); + Route::bind('categoryCode', fn (string $categoryCode) => CategoryBackedEnum::fromCode($categoryCode) ?? abort(404)); + + Route::post('/categories-code/{categoryCode}', function (CategoryBackedEnum $categoryCode) { + return $categoryCode->value; + })->middleware(['web']); + $response = $this->post('/categories/fruits'); $response->assertSee('fruits'); @@ -68,7 +72,7 @@ public function testWithoutRouteCachingEnabled() $response->assertSee('people'); $response = $this->post('/categories/cars'); - $response->assertNotFound(404); + $response->assertNotFound(); $response = $this->post('/categories-default/'); $response->assertSee('fruits'); @@ -78,5 +82,14 @@ public function testWithoutRouteCachingEnabled() $response = $this->post('/categories-default/fruits'); $response->assertSee('fruits'); + + $response = $this->post('/categories-code/c01'); + $response->assertSee('people'); + + $response = $this->post('/categories-code/c02'); + $response->assertSee('fruits'); + + $response = $this->post('/categories-code/00'); + $response->assertNotFound(); } } diff --git a/tests/Integration/Session/CookieSessionHandlerTest.php b/tests/Integration/Session/CookieSessionHandlerTest.php index e7307d71421a..896cf25cf5c5 100644 --- a/tests/Integration/Session/CookieSessionHandlerTest.php +++ b/tests/Integration/Session/CookieSessionHandlerTest.php @@ -20,7 +20,7 @@ public function testCookieSessionDriverCookiesCanExpireOnClose() $this->assertEquals(0, $sessionValueCookie->getExpiresTime()); } - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $app['config']->set('app.key', Str::random(32)); $app['config']->set('session.driver', 'cookie'); diff --git a/tests/Integration/Session/SessionPersistenceTest.php b/tests/Integration/Session/SessionPersistenceTest.php index d911bdffae0e..6253b79eb766 100644 --- a/tests/Integration/Session/SessionPersistenceTest.php +++ b/tests/Integration/Session/SessionPersistenceTest.php @@ -31,7 +31,7 @@ public function testSessionIsPersistedEvenIfExceptionIsThrownFromRoute() $this->assertTrue($handler->written); } - protected function getEnvironmentSetUp($app) + protected function defineEnvironment($app) { $app->instance( ExceptionHandler::class, diff --git a/tests/Integration/Translation/TranslatorTest.php b/tests/Integration/Translation/TranslatorTest.php index 5e374e82d1cb..de82aabce403 100644 --- a/tests/Integration/Translation/TranslatorTest.php +++ b/tests/Integration/Translation/TranslatorTest.php @@ -70,4 +70,17 @@ public function testItCanHandleMissingKeysNoReturn() $this->app['translator']->handleMissingKeysUsing(null); } + + public function testItReturnsCorrectLocaleForMissingKeys() + { + $this->app['translator']->handleMissingKeysUsing(function ($key, $replacements, $locale) { + $_SERVER['__missing_translation_key_locale'] = $locale; + }); + + $this->app['translator']->get('some missing key', [], 'ht'); + + $this->assertSame('ht', $_SERVER['__missing_translation_key_locale']); + + $this->app['translator']->handleMissingKeysUsing(null); + } } diff --git a/tests/Mail/MailLogTransportTest.php b/tests/Mail/MailLogTransportTest.php index 3bb0c69a44df..33367d52610c 100644 --- a/tests/Mail/MailLogTransportTest.php +++ b/tests/Mail/MailLogTransportTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Mail; +use Illuminate\Mail\Attachment; use Illuminate\Mail\Message; use Illuminate\Mail\Transport\LogTransport; use Monolog\Handler\StreamHandler; @@ -59,6 +60,32 @@ public function testItDecodesTheMessageBeforeLogging() $this->assertStringContainsString('https://example.com/reset-password=5e113c71a4c210aff04b3fa66f1b1299', $actualLoggedValue); } + public function testItOnlyDecodesQuotedPrintablePartsOfTheMessageBeforeLogging() + { + $message = (new Message(new Email)) + ->from('noreply@example.com', 'no-reply') + ->to('taylor@example.com', 'Taylor') + ->html(<<<'BODY' + Hi, + + Click here to reset your password. + + All the best, + + Burt & Irving + BODY) + ->text('A text part') + ->attach(Attachment::fromData(fn () => 'My attachment', 'attachment.txt')); + + $actualLoggedValue = $this->getLoggedEmailMessage($message); + + $this->assertStringContainsString('href=', $actualLoggedValue); + $this->assertStringContainsString('Burt & Irving', $actualLoggedValue); + $this->assertStringContainsString('https://example.com/reset-password=5e113c71a4c210aff04b3fa66f1b1299', $actualLoggedValue); + $this->assertStringContainsString('name=attachment.txt', $actualLoggedValue); + $this->assertStringContainsString('filename=attachment.txt', $actualLoggedValue); + } + public function testGetLogTransportWithPsrLogger() { $this->app['config']->set('mail.driver', 'log'); diff --git a/tests/Mail/MailSesV2TransportTest.php b/tests/Mail/MailSesV2TransportTest.php index 7b7821558ac7..a433eae2e88e 100755 --- a/tests/Mail/MailSesV2TransportTest.php +++ b/tests/Mail/MailSesV2TransportTest.php @@ -73,7 +73,7 @@ public function testSend() ->with(m::on(function ($arg) { return $arg['Source'] === 'myself@example.com' && $arg['Destination']['ToAddresses'] === ['me@example.com', 'you@example.com'] && - $arg['Tags'] === [['Name' => 'FooTag', 'Value' => 'TagValue']] && + $arg['EmailTags'] === [['Name' => 'FooTag', 'Value' => 'TagValue']] && strpos($arg['Content']['Raw']['Data'], 'Reply-To: Taylor Otwell ') !== false; })) ->andReturn($sesResult); @@ -111,7 +111,7 @@ public function testSesV2LocalConfiguration() 'region' => 'eu-west-1', 'options' => [ 'ConfigurationSetName' => 'Laravel', - 'Tags' => [ + 'EmailTags' => [ ['Name' => 'Laravel', 'Value' => 'Framework'], ], ], @@ -144,7 +144,7 @@ public function testSesV2LocalConfiguration() $this->assertSame([ 'ConfigurationSetName' => 'Laravel', - 'Tags' => [ + 'EmailTags' => [ ['Name' => 'Laravel', 'Value' => 'Framework'], ], ], $transport->getOptions()); diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 9908b1a76de3..4c914b2e9698 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -7,6 +7,7 @@ use Illuminate\Process\Exceptions\ProcessTimedOutException; use Illuminate\Process\Factory; use OutOfBoundsException; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -148,6 +149,26 @@ public function testBasicProcessFake() $this->assertTrue($result->successful()); } + public function testBasicProcessFakeWithMultiLineCommand() + { + $factory = new Factory; + + $factory->preventStrayProcesses(); + + $factory->fake([ + '*' => 'The output', + ]); + + $result = $factory->run(<<<'COMMAND' + git clone --depth 1 \ + --single-branch \ + --branch main \ + git://some-url . + COMMAND); + + $this->assertSame(0, $result->exitCode()); + } + public function testProcessFakeExitCodes() { $factory = new Factory; @@ -404,12 +425,9 @@ public function testFakeProcessesDontThrowIfFalse() $this->assertTrue(true); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanHaveErrorOutput() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory; $result = $factory->path(__DIR__)->run('echo "Hello World" >&2; exit 1;'); @@ -435,12 +453,9 @@ public function testFakeProcessesCanThrowWithoutOutput() $result->throw(); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanThrowWithoutOutput() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $this->expectException(ProcessFailedException::class); $this->expectExceptionMessage(<<<'EOT' The command "exit 1;" failed. @@ -476,12 +491,9 @@ public function testFakeProcessesCanThrowWithErrorOutput() $result->throw(); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanThrowWithErrorOutput() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $this->expectException(ProcessFailedException::class); $this->expectExceptionMessage(<<<'EOT' The command "echo "Hello World" >&2; exit 1;" failed. @@ -521,12 +533,9 @@ public function testFakeProcessesCanThrowWithOutput() $result->throw(); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanThrowWithOutput() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $this->expectException(ProcessFailedException::class); $this->expectExceptionMessage(<<<'EOT' The command "echo "Hello World" >&1; exit 1;" failed. @@ -545,12 +554,9 @@ public function testRealProcessesCanThrowWithOutput() $result->throw(); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanTimeout() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $this->expectException(ProcessTimedOutException::class); $this->expectExceptionMessage( 'The process "sleep 2; exit 1;" exceeded the timeout of 1 seconds.' @@ -562,12 +568,9 @@ public function testRealProcessesCanTimeout() $result->throw(); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanThrowIfTrue() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $this->expectException(ProcessFailedException::class); $factory = new Factory; @@ -576,12 +579,9 @@ public function testRealProcessesCanThrowIfTrue() $result->throwIf(true); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesDoesntThrowIfFalse() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory; $result = $factory->path(__DIR__)->run('echo "Hello World" >&2; exit 1;'); @@ -590,24 +590,18 @@ public function testRealProcessesDoesntThrowIfFalse() $this->assertTrue(true); } + #[RequiresOperatingSystem('Linux|DAR')] public function testRealProcessesCanUseStandardInput() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory(); $result = $factory->input('foobar')->run('cat'); $this->assertSame('foobar', $result->output()); } + #[RequiresOperatingSystem('Linux|DAR')] public function testProcessPipe() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory; $factory->fake([ 'cat *' => "Hello, world\nfoo\nbar", @@ -621,12 +615,9 @@ public function testProcessPipe() $this->assertSame("foo\n", $pipe->output()); } + #[RequiresOperatingSystem('Linux|DAR')] public function testProcessPipeFailed() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory; $factory->fake([ 'cat *' => $factory->result(exitCode: 1), @@ -640,12 +631,9 @@ public function testProcessPipeFailed() $this->assertTrue($pipe->failed()); } + #[RequiresOperatingSystem('Linux|DAR')] public function testProcessSimplePipe() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory; $factory->fake([ 'cat *' => "Hello, world\nfoo\nbar", @@ -659,12 +647,9 @@ public function testProcessSimplePipe() $this->assertSame("foo\n", $pipe->output()); } + #[RequiresOperatingSystem('Linux|DAR')] public function testProcessSimplePipeFailed() { - if (windows_os()) { - $this->markTestSkipped('Requires Linux.'); - } - $factory = new Factory; $factory->fake([ 'cat *' => $factory->result(exitCode: 1), diff --git a/tests/Queue/QueueBeanstalkdQueueTest.php b/tests/Queue/QueueBeanstalkdQueueTest.php index ed4e6f904e20..beab4bfa98df 100755 --- a/tests/Queue/QueueBeanstalkdQueueTest.php +++ b/tests/Queue/QueueBeanstalkdQueueTest.php @@ -45,7 +45,7 @@ public function testPushProperlyPushesJobOntoBeanstalkd() $this->queue->push('foo', ['data'], 'stack'); $this->queue->push('foo', ['data']); - $this->container->shouldHaveReceived('bound')->with('events')->times(2); + $this->container->shouldHaveReceived('bound')->with('events')->times(4); Str::createUuidsNormally(); } @@ -67,7 +67,7 @@ public function testDelayedPushProperlyPushesJobOntoBeanstalkd() $this->queue->later(5, 'foo', ['data'], 'stack'); $this->queue->later(5, 'foo', ['data']); - $this->container->shouldHaveReceived('bound')->with('events')->times(2); + $this->container->shouldHaveReceived('bound')->with('events')->times(4); Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueDatabaseQueueIntegrationTest.php b/tests/Queue/QueueDatabaseQueueIntegrationTest.php index 259fe46adcbe..4c4f7c91c5c1 100644 --- a/tests/Queue/QueueDatabaseQueueIntegrationTest.php +++ b/tests/Queue/QueueDatabaseQueueIntegrationTest.php @@ -9,6 +9,7 @@ use Illuminate\Events\Dispatcher; use Illuminate\Queue\DatabaseQueue; use Illuminate\Queue\Events\JobQueued; +use Illuminate\Queue\Events\JobQueueing; use Illuminate\Support\Carbon; use Illuminate\Support\Str; use PHPUnit\Framework\TestCase; @@ -247,21 +248,28 @@ public function testThatReservedJobsAreNotPopped() $this->assertNull($popped_job); } - public function testJobPayloadIsAvailableOnEvent() + public function testJobPayloadIsAvailableOnEvents() { - $event = null; + $jobQueueingEvent = null; + $jobQueuedEvent = null; Str::createUuidsUsingSequence([ 'expected-job-uuid', ]); - $this->container['events']->listen(function (JobQueued $e) use (&$event) { - $event = $e; + $this->container['events']->listen(function (JobQueueing $e) use (&$jobQueueingEvent) { + $jobQueueingEvent = $e; + }); + $this->container['events']->listen(function (JobQueued $e) use (&$jobQueuedEvent) { + $jobQueuedEvent = $e; }); $this->queue->push('MyJob', [ 'laravel' => 'Framework', ]); - $this->assertIsArray($event->payload()); - $this->assertSame('expected-job-uuid', $event->payload()['uuid']); + $this->assertIsArray($jobQueueingEvent->payload()); + $this->assertSame('expected-job-uuid', $jobQueueingEvent->payload()['uuid']); + + $this->assertIsArray($jobQueuedEvent->payload()); + $this->assertSame('expected-job-uuid', $jobQueuedEvent->payload()['uuid']); } } diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 17087db4e592..53c65720551c 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -46,7 +46,7 @@ public function testPushProperlyPushesJobOntoDatabase($uuid, $job, $displayNameS $queue->push($job, ['data']); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Str::createUuidsNormally(); } @@ -87,7 +87,7 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() $queue->later(10, 'foo', ['data']); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 442676de71ce..007f743653d8 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -35,7 +35,7 @@ public function testPushProperlyPushesJobOntoRedis() $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Str::createUuidsNormally(); } @@ -60,7 +60,7 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook() $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Queue::createPayloadUsing(null); @@ -91,7 +91,7 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook() $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Queue::createPayloadUsing(null); @@ -120,7 +120,7 @@ public function testDelayedPushProperlyPushesJobOntoRedis() $id = $queue->later(1, 'foo', ['data']); $this->assertSame('foo', $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Str::createUuidsNormally(); } @@ -147,7 +147,7 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis() ); $queue->later($date, 'foo', ['data']); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 3886a3f83854..021e66484b68 100755 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -116,7 +116,7 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoSqs() $this->sqs->shouldReceive('sendMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload, 'DelaySeconds' => 5])->andReturn($this->mockedSendMessageResponseModel); $id = $queue->later($now->addSeconds(5), $this->mockedJob, $this->mockedData, $this->queueName); $this->assertEquals($this->mockedMessageId, $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); } public function testDelayedPushProperlyPushesJobOntoSqs() @@ -129,7 +129,7 @@ public function testDelayedPushProperlyPushesJobOntoSqs() $this->sqs->shouldReceive('sendMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload, 'DelaySeconds' => $this->mockedDelay])->andReturn($this->mockedSendMessageResponseModel); $id = $queue->later($this->mockedDelay, $this->mockedJob, $this->mockedData, $this->queueName); $this->assertEquals($this->mockedMessageId, $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); } public function testPushProperlyPushesJobOntoSqs() @@ -141,7 +141,7 @@ public function testPushProperlyPushesJobOntoSqs() $this->sqs->shouldReceive('sendMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'MessageBody' => $this->mockedPayload])->andReturn($this->mockedSendMessageResponseModel); $id = $queue->push($this->mockedJob, $this->mockedData, $this->queueName); $this->assertEquals($this->mockedMessageId, $id); - $container->shouldHaveReceived('bound')->with('events')->once(); + $container->shouldHaveReceived('bound')->with('events')->twice(); } public function testSizeProperlyReadsSqsQueueSize() diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index 2c6079adf36b..c612affaa44b 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -764,7 +764,7 @@ public function testSignedUrl() $this->assertTrue($url->hasValidSignature($request)); - $request = Request::create($url->signedRoute('foo').'?tempered=true'); + $request = Request::create($url->signedRoute('foo').'?tampered=true'); $this->assertFalse($url->hasValidSignature($request)); } @@ -812,7 +812,7 @@ public function testSignedRelativeUrl() $this->assertTrue($url->hasValidSignature($request, false)); - $request = Request::create($url->signedRoute('foo', [], null, false).'?tempered=true'); + $request = Request::create($url->signedRoute('foo', [], null, false).'?tampered=true'); $this->assertFalse($url->hasValidSignature($request, false)); } @@ -891,7 +891,7 @@ public function testSignedUrlWithKeyResolver() $this->assertTrue($url->hasValidSignature($request)); - $request = Request::create($url->signedRoute('foo').'?tempered=true'); + $request = Request::create($url->signedRoute('foo').'?tampered=true'); $this->assertFalse($url->hasValidSignature($request)); diff --git a/tests/Support/ConfigurationUrlParserTest.php b/tests/Support/ConfigurationUrlParserTest.php index 9035d08a45b6..22edc49810fe 100644 --- a/tests/Support/ConfigurationUrlParserTest.php +++ b/tests/Support/ConfigurationUrlParserTest.php @@ -135,7 +135,7 @@ public static function databaseUrls() ], ], 'query params from URL are used as extra params' => [ - 'url' => 'mysql://foo:bar@localhost/database?charset=UTF-8', + 'mysql://foo:bar@localhost/database?charset=UTF-8', [ 'driver' => 'mysql', 'database' => 'database', diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index d43b96a0d7f7..957d3c20c99e 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -5,8 +5,10 @@ use Carbon\CarbonInterval; use Exception; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Date; use Illuminate\Support\Sleep; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -563,4 +565,69 @@ public function testItDoesntRunCallbacksWhenNotFaking() $this->assertTrue(true); } + + public function testItDoesNotSyncCarbon() + { + Carbon::setTestNow('2000-01-01 00:00:00'); + Sleep::fake(); + + Sleep::for(5)->minutes() + ->and(3)->seconds(); + + Sleep::assertSequence([ + Sleep::for(303)->seconds(), + ]); + $this->assertSame('2000-01-01 00:00:00', Date::now()->toDateTimeString()); + } + + public function testItCanSyncCarbon() + { + Carbon::setTestNow('2000-01-01 00:00:00'); + Sleep::fake(); + Sleep::syncWithCarbon(); + + Sleep::for(5)->minutes() + ->and(3)->seconds(); + + Sleep::assertSequence([ + Sleep::for(303)->seconds(), + ]); + $this->assertSame('2000-01-01 00:05:03', Date::now()->toDateTimeString()); + } + + #[TestWith([ + 'syncWithCarbon' => true, + 'datetime' => '2000-01-01 00:05:03', + ])] + #[TestWith([ + 'syncWithCarbon' => false, + 'datetime' => '2000-01-01 00:00:00', + ])] + public function testFakeCanSetSyncWithCarbon(bool $syncWithCarbon, string $datetime) + { + Carbon::setTestNow('2000-01-01 00:00:00'); + Sleep::fake(syncWithCarbon: $syncWithCarbon); + + Sleep::for(5)->minutes() + ->and(3)->seconds(); + + Sleep::assertSequence([ + Sleep::for(303)->seconds(), + ]); + $this->assertSame($datetime, Date::now()->toDateTimeString()); + } + + public function testFakeDoesNotNeedToSyncWithCarbon() + { + Carbon::setTestNow('2000-01-01 00:00:00'); + Sleep::fake(); + + Sleep::for(5)->minutes() + ->and(3)->seconds(); + + Sleep::assertSequence([ + Sleep::for(303)->seconds(), + ]); + $this->assertSame('2000-01-01 00:00:00', Date::now()->toDateTimeString()); + } } diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index 083ca5bbb306..119dd2a3bd65 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -1260,4 +1260,53 @@ public function testPrependKeysWith() ], ], Arr::prependKeysWith($array, 'test.')); } + + public function testTake() + { + $array = [1, 2, 3, 4, 5, 6]; + + $this->assertEquals([ + 1, 2, 3, + ], Arr::take($array, 3)); + + $this->assertEquals([ + 4, 5, 6, + ], Arr::take($array, -3)); + } + + public function testSelect() + { + $array = [ + [ + 'name' => 'Taylor', + 'role' => 'Developer', + 'age' => 1, + ], + [ + 'name' => 'Abigail', + 'role' => 'Infrastructure', + 'age' => 2, + ], + ]; + + $this->assertEquals([ + [ + 'name' => 'Taylor', + 'age' => 1, + ], + [ + 'name' => 'Abigail', + 'age' => 2, + ], + ], Arr::select($array, ['name', 'age'])); + + $this->assertEquals([ + [ + 'name' => 'Taylor', + ], + [ + 'name' => 'Abigail', + ], + ], Arr::select($array, 'name')); + } } diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index dc82699b9329..e1e0a2d89cdb 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -391,6 +391,34 @@ public function testShiftReturnsAndRemovesFirstXItemsInCollection() $this->assertSame('baz', $data->first()); $this->assertEquals(new Collection(['foo', 'bar', 'baz']), (new Collection(['foo', 'bar', 'baz']))->shift(6)); + + $data = new Collection(['foo', 'bar', 'baz']); + + $this->assertEquals(new Collection([]), $data->shift(0)); + $this->assertEquals(collect(['foo', 'bar', 'baz']), $data); + + $this->expectException('InvalidArgumentException'); + (new Collection(['foo', 'bar', 'baz']))->shift(-1); + + $this->expectException('InvalidArgumentException'); + (new Collection(['foo', 'bar', 'baz']))->shift(-2); + } + + public function testShiftReturnsNullOnEmptyCollection() + { + $itemFoo = new \stdClass(); + $itemFoo->text = 'f'; + $itemBar = new \stdClass(); + $itemBar->text = 'x'; + + $items = collect([$itemFoo, $itemBar]); + + $foo = $items->shift(); + $bar = $items->shift(); + + $this->assertSame('f', $foo?->text); + $this->assertSame('x', $bar?->text); + $this->assertNull($items->shift()); } /** @@ -2031,9 +2059,9 @@ public function testSortByString($collection) $this->assertEquals([['name' => 'dayle'], ['name' => 'taylor']], array_values($data->all())); $data = new $collection([['name' => 'taylor'], ['name' => 'dayle']]); - $data = $data->sortBy('name', SORT_STRING); + $data = $data->sortBy('name', SORT_STRING, true); - $this->assertEquals([['name' => 'dayle'], ['name' => 'taylor']], array_values($data->all())); + $this->assertEquals([['name' => 'taylor'], ['name' => 'dayle']], array_values($data->all())); } /** @@ -2047,6 +2075,21 @@ public function testSortByCallableString($collection) $this->assertEquals([['sort' => 1], ['sort' => 2]], array_values($data->all())); } + #[DataProvider('collectionClassProvider')] + public function testSortByCallableStringDesc($collection) + { + $data = new $collection([['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar']]); + $data = $data->sortByDesc(['id']); + $this->assertEquals([['id' => 2, 'name' => 'bar'], ['id' => 1, 'name' => 'foo']], array_values($data->all())); + + $data = new $collection([['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar'], ['id' => 2, 'name' => 'baz']]); + $data = $data->sortByDesc(['id']); + $this->assertEquals([['id' => 2, 'name' => 'bar'], ['id' => 2, 'name' => 'baz'], ['id' => 1, 'name' => 'foo']], array_values($data->all())); + + $data = $data->sortByDesc(['id', 'name']); + $this->assertEquals([['id' => 2, 'name' => 'baz'], ['id' => 2, 'name' => 'bar'], ['id' => 1, 'name' => 'foo']], array_values($data->all())); + } + /** * @dataProvider collectionClassProvider */ @@ -2077,6 +2120,92 @@ public function testSortByAlwaysReturnsAssoc($collection) $this->assertEquals([1 => ['sort' => 1], 0 => ['sort' => 2]], $data->all()); } + #[DataProvider('collectionClassProvider')] + public function testSortByMany($collection) + { + $defaultLocale = setlocale(LC_ALL, 0); + + $data = new $collection([['item' => '1'], ['item' => '10'], ['item' => 5], ['item' => 20]]); + $expected = $data->pluck('item')->toArray(); + + sort($expected); + $data = $data->sortBy(['item']); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + rsort($expected); + $data = $data->sortBy([['item', 'desc']]); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_STRING); + $data = $data->sortBy(['item'], SORT_STRING); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + rsort($expected, SORT_STRING); + $data = $data->sortBy([['item', 'desc']], SORT_STRING); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_NUMERIC); + $data = $data->sortBy(['item'], SORT_NUMERIC); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + rsort($expected, SORT_NUMERIC); + $data = $data->sortBy([['item', 'desc']], SORT_NUMERIC); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + $data = new $collection([['item' => 'img1'], ['item' => 'img101'], ['item' => 'img10'], ['item' => 'img11']]); + $expected = $data->pluck('item')->toArray(); + + sort($expected, SORT_NUMERIC); + $data = $data->sortBy(['item'], SORT_NUMERIC); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected); + $data = $data->sortBy(['item']); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_NATURAL); + $data = $data->sortBy(['item'], SORT_NATURAL); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + $data = new $collection([['item' => 'img1'], ['item' => 'Img101'], ['item' => 'img10'], ['item' => 'Img11']]); + $expected = $data->pluck('item')->toArray(); + + sort($expected); + $data = $data->sortBy(['item']); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_NATURAL | SORT_FLAG_CASE); + $data = $data->sortBy(['item'], SORT_NATURAL | SORT_FLAG_CASE); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_FLAG_CASE | SORT_STRING); + $data = $data->sortBy(['item'], SORT_FLAG_CASE | SORT_STRING); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_FLAG_CASE | SORT_NUMERIC); + $data = $data->sortBy(['item'], SORT_FLAG_CASE | SORT_NUMERIC); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + $data = new $collection([['item' => 'Österreich'], ['item' => 'Oesterreich'], ['item' => 'Zeta']]); + $expected = $data->pluck('item')->toArray(); + + sort($expected); + $data = $data->sortBy(['item']); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + sort($expected, SORT_LOCALE_STRING); + $data = $data->sortBy(['item'], SORT_LOCALE_STRING); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + setlocale(LC_ALL, 'de_DE'); + + sort($expected, SORT_LOCALE_STRING); + $data = $data->sortBy(['item'], SORT_LOCALE_STRING); + $this->assertEquals($data->pluck('item')->toArray(), $expected); + + setlocale(LC_ALL, $defaultLocale); + } + /** * @dataProvider collectionClassProvider */ @@ -4228,6 +4357,99 @@ public function testOnly($collection) $this->assertEquals(['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], $data->only(collect(['first', 'email']))->all()); } + /** + * @dataProvider collectionClassProvider + */ + public function testSelectWithArrays($collection) + { + $data = new $collection([ + ['first' => 'Taylor', 'last' => 'Otwell', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'last' => 'Archer', 'email' => 'jessarcher@gmail.com'], + ]); + + $this->assertEquals($data->all(), $data->select(null)->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select(['first', 'missing'])->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select('first', 'missing')->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select(collect(['first', 'missing']))->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select(['first', 'email'])->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select('first', 'email')->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select(collect(['first', 'email']))->all()); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSelectWithArrayAccess($collection) + { + $data = new $collection([ + new TestArrayAccessImplementation(['first' => 'Taylor', 'last' => 'Otwell', 'email' => 'taylorotwell@gmail.com']), + new TestArrayAccessImplementation(['first' => 'Jess', 'last' => 'Archer', 'email' => 'jessarcher@gmail.com']), + ]); + + $this->assertEquals($data->all(), $data->select(null)->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select(['first', 'missing'])->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select('first', 'missing')->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select(collect(['first', 'missing']))->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select(['first', 'email'])->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select('first', 'email')->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select(collect(['first', 'email']))->all()); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSelectWithObjects($collection) + { + $data = new $collection([ + (object) ['first' => 'Taylor', 'last' => 'Otwell', 'email' => 'taylorotwell@gmail.com'], + (object) ['first' => 'Jess', 'last' => 'Archer', 'email' => 'jessarcher@gmail.com'], + ]); + + $this->assertEquals($data->all(), $data->select(null)->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select(['first', 'missing'])->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select('first', 'missing')->all()); + $this->assertEquals([['first' => 'Taylor'], ['first' => 'Jess']], $data->select(collect(['first', 'missing']))->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select(['first', 'email'])->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select('first', 'email')->all()); + + $this->assertEquals([ + ['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], + ['first' => 'Jess', 'email' => 'jessarcher@gmail.com'], + ], $data->select(collect(['first', 'email']))->all()); + } + /** * @dataProvider collectionClassProvider */ diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index bc4d21b1239c..24825cdd7308 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -1,307 +1,311 @@ assertSame('0', Number::format(0)); - $this->assertSame('0', Number::format(0.0)); - $this->assertSame('0', Number::format(0.00)); - $this->assertSame('1', Number::format(1)); - $this->assertSame('10', Number::format(10)); - $this->assertSame('25', Number::format(25)); - $this->assertSame('100', Number::format(100)); - $this->assertSame('100,000', Number::format(100000)); - $this->assertSame('100,000.00', Number::format(100000, precision: 2)); - $this->assertSame('100,000.12', Number::format(100000.123, precision: 2)); - $this->assertSame('100,000.123', Number::format(100000.1234, maxPrecision: 3)); - $this->assertSame('100,000.124', Number::format(100000.1236, maxPrecision: 3)); - $this->assertSame('123,456,789', Number::format(123456789)); - - $this->assertSame('-1', Number::format(-1)); - $this->assertSame('-10', Number::format(-10)); - $this->assertSame('-25', Number::format(-25)); - - $this->assertSame('0.2', Number::format(0.2)); - $this->assertSame('0.20', Number::format(0.2, precision: 2)); - $this->assertSame('0.123', Number::format(0.1234, maxPrecision: 3)); - $this->assertSame('1.23', Number::format(1.23)); - $this->assertSame('-1.23', Number::format(-1.23)); - $this->assertSame('123.456', Number::format(123.456)); - - $this->assertSame('∞', Number::format(INF)); - $this->assertSame('NaN', Number::format(NAN)); - } + static::ensureIntlExtensionIsInstalled(); - #[RequiresPhpExtension('intl')] - public function testFormatWithDifferentLocale() - { - $this->assertSame('123,456,789', Number::format(123456789, locale: 'en')); - $this->assertSame('123.456.789', Number::format(123456789, locale: 'de')); - $this->assertSame('123 456 789', Number::format(123456789, locale: 'fr')); - $this->assertSame('123 456 789', Number::format(123456789, locale: 'ru')); - $this->assertSame('123 456 789', Number::format(123456789, locale: 'sv')); + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::DECIMAL); + + if (! is_null($maxPrecision)) { + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $maxPrecision); + } elseif (! is_null($precision)) { + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); + } + + return $formatter->format($number); } - #[RequiresPhpExtension('intl')] - public function testFormatWithAppLocale() + /** + * Spell out the given number in the given locale. + * + * @param int|float $number + * @param string|null $locale + * @param int|null $after + * @param int|null $until + * @return string + */ + public static function spell(int|float $number, ?string $locale = null, ?int $after = null, ?int $until = null) { - $this->assertSame('123,456,789', Number::format(123456789)); + static::ensureIntlExtensionIsInstalled(); + + if (! is_null($after) && $number <= $after) { + return static::format($number, locale: $locale); + } - Number::useLocale('de'); + if (! is_null($until) && $number >= $until) { + return static::format($number, locale: $locale); + } - $this->assertSame('123.456.789', Number::format(123456789)); + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::SPELLOUT); - Number::useLocale('en'); + return $formatter->format($number); } - public function testSpellout() + /** + * Convert the given number to ordinal form. + * + * @param int|float $number + * @param string|null $locale + * @return string + */ + public static function ordinal(int|float $number, ?string $locale = null) { - $this->assertSame('ten', Number::spell(10)); - $this->assertSame('one point two', Number::spell(1.2)); + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::ORDINAL); + + return $formatter->format($number); } - #[RequiresPhpExtension('intl')] - public function testSpelloutWithLocale() + /** + * Convert the given number to its percentage equivalent. + * + * @param int|float $number + * @param int $precision + * @param int|null $maxPrecision + * @param string|null $locale + * @return string|false + */ + public static function percentage(int|float $number, int $precision = 0, ?int $maxPrecision = null, ?string $locale = null) { - $this->assertSame('trois', Number::spell(3, 'fr')); + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::PERCENT); + + if (! is_null($maxPrecision)) { + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $maxPrecision); + } else { + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision); + } + + return $formatter->format($number / 100); } - #[RequiresPhpExtension('intl')] - public function testSpelloutWithThreshold() + /** + * Convert the given number to its currency equivalent. + * + * @param int|float $number + * @param string $in + * @param string|null $locale + * @return string|false + */ + public static function currency(int|float $number, string $in = 'USD', ?string $locale = null) { - $this->assertSame('9', Number::spell(9, after: 10)); - $this->assertSame('10', Number::spell(10, after: 10)); - $this->assertSame('eleven', Number::spell(11, after: 10)); + static::ensureIntlExtensionIsInstalled(); - $this->assertSame('nine', Number::spell(9, until: 10)); - $this->assertSame('10', Number::spell(10, until: 10)); - $this->assertSame('11', Number::spell(11, until: 10)); + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::CURRENCY); - $this->assertSame('ten thousand', Number::spell(10000, until: 50000)); - $this->assertSame('100,000', Number::spell(100000, until: 50000)); + return $formatter->formatCurrency($number, $in); } - public function testOrdinal() + /** + * Convert the given number to its file size equivalent. + * + * @param int|float $bytes + * @param int $precision + * @param int|null $maxPrecision + * @return string + */ + public static function fileSize(int|float $bytes, int $precision = 0, ?int $maxPrecision = null) { - $this->assertSame('1st', Number::ordinal(1)); - $this->assertSame('2nd', Number::ordinal(2)); - $this->assertSame('3rd', Number::ordinal(3)); + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + for ($i = 0; ($bytes / 1024) > 0.9 && ($i < count($units) - 1); $i++) { + $bytes /= 1024; + } + + return sprintf('%s %s', static::format($bytes, $precision, $maxPrecision), $units[$i]); } - #[RequiresPhpExtension('intl')] - public function testToPercent() + /** + * Convert the number to its human-readable equivalent. + * + * @param int|float $number + * @param int $precision + * @param int|null $maxPrecision + * @return bool|string + */ + public static function abbreviate(int|float $number, int $precision = 0, ?int $maxPrecision = null) { - $this->assertSame('0%', Number::percentage(0, precision: 0)); - $this->assertSame('0%', Number::percentage(0)); - $this->assertSame('1%', Number::percentage(1)); - $this->assertSame('10.00%', Number::percentage(10, precision: 2)); - $this->assertSame('100%', Number::percentage(100)); - $this->assertSame('100.00%', Number::percentage(100, precision: 2)); - $this->assertSame('100.123%', Number::percentage(100.1234, maxPrecision: 3)); - - $this->assertSame('300%', Number::percentage(300)); - $this->assertSame('1,000%', Number::percentage(1000)); - - $this->assertSame('2%', Number::percentage(1.75)); - $this->assertSame('1.75%', Number::percentage(1.75, precision: 2)); - $this->assertSame('1.750%', Number::percentage(1.75, precision: 3)); - $this->assertSame('0%', Number::percentage(0.12345)); - $this->assertSame('0.00%', Number::percentage(0, precision: 2)); - $this->assertSame('0.12%', Number::percentage(0.12345, precision: 2)); - $this->assertSame('0.1235%', Number::percentage(0.12345, precision: 4)); + return static::forHumans($number, $precision, $maxPrecision, abbreviate: true); } - #[RequiresPhpExtension('intl')] - public function testToCurrency() + /** + * Convert the number to its human-readable equivalent. + * + * @param int|float $number + * @param int $precision + * @param int|null $maxPrecision + * @param bool $abbreviate + * @return bool|string + */ + public static function forHumans(int|float $number, int $precision = 0, ?int $maxPrecision = null, bool $abbreviate = false) { - $this->assertSame('$0.00', Number::currency(0)); - $this->assertSame('$1.00', Number::currency(1)); - $this->assertSame('$10.00', Number::currency(10)); - - $this->assertSame('€0.00', Number::currency(0, 'EUR')); - $this->assertSame('€1.00', Number::currency(1, 'EUR')); - $this->assertSame('€10.00', Number::currency(10, 'EUR')); - - $this->assertSame('-$5.00', Number::currency(-5)); - $this->assertSame('$5.00', Number::currency(5.00)); - $this->assertSame('$5.32', Number::currency(5.325)); + return static::summarize($number, $precision, $maxPrecision, $abbreviate ? [ + 3 => 'K', + 6 => 'M', + 9 => 'B', + 12 => 'T', + 15 => 'Q', + ] : [ + 3 => ' thousand', + 6 => ' million', + 9 => ' billion', + 12 => ' trillion', + 15 => ' quadrillion', + ]); } - #[RequiresPhpExtension('intl')] - public function testToCurrencyWithDifferentLocale() + /** + * Convert the number to its human-readable equivalent. + * + * @param int|float $number + * @param int $precision + * @param int|null $maxPrecision + * @param array $units + * @return string|false + */ + protected static function summarize(int|float $number, int $precision = 0, ?int $maxPrecision = null, array $units = []) { - $this->assertSame('1,00 €', Number::currency(1, 'EUR', 'de')); - $this->assertSame('1,00 $', Number::currency(1, 'USD', 'de')); - $this->assertSame('1,00 £', Number::currency(1, 'GBP', 'de')); - - $this->assertSame('123.456.789,12 $', Number::currency(123456789.12345, 'USD', 'de')); - $this->assertSame('123.456.789,12 €', Number::currency(123456789.12345, 'EUR', 'de')); - $this->assertSame('1 234,56 $US', Number::currency(1234.56, 'USD', 'fr')); + if (empty($units)) { + $units = [ + 3 => 'K', + 6 => 'M', + 9 => 'B', + 12 => 'T', + 15 => 'Q', + ]; + } + + switch (true) { + case floatval($number) === 0.0: + return $precision > 0 ? static::format(0, $precision, $maxPrecision) : '0'; + case $number < 0: + return sprintf('-%s', static::summarize(abs($number), $precision, $maxPrecision, $units)); + case $number >= 1e15: + return sprintf('%s'.end($units), static::summarize($number / 1e15, $precision, $maxPrecision, $units)); + } + + $numberExponent = floor(log10($number)); + $displayExponent = $numberExponent - ($numberExponent % 3); + $number /= pow(10, $displayExponent); + + return trim(sprintf('%s%s', static::format($number, $precision, $maxPrecision), $units[$displayExponent] ?? '')); } - public function testBytesToHuman() + /** + * Clamp the given number between the given minimum and maximum. + * + * @param int|float $number + * @param int|float $min + * @param int|float $max + * @return int|float + */ + public static function clamp(int|float $number, int|float $min, int|float $max) { - $this->assertSame('0 B', Number::fileSize(0)); - $this->assertSame('0.00 B', Number::fileSize(0, precision: 2)); - $this->assertSame('1 B', Number::fileSize(1)); - $this->assertSame('1 KB', Number::fileSize(1024)); - $this->assertSame('2 KB', Number::fileSize(2048)); - $this->assertSame('2.00 KB', Number::fileSize(2048, precision: 2)); - $this->assertSame('1.23 KB', Number::fileSize(1264, precision: 2)); - $this->assertSame('1.234 KB', Number::fileSize(1264.12345, maxPrecision: 3)); - $this->assertSame('1.234 KB', Number::fileSize(1264, 3)); - $this->assertSame('5 GB', Number::fileSize(1024 * 1024 * 1024 * 5)); - $this->assertSame('10 TB', Number::fileSize((1024 ** 4) * 10)); - $this->assertSame('10 PB', Number::fileSize((1024 ** 5) * 10)); - $this->assertSame('1 ZB', Number::fileSize(1024 ** 7)); - $this->assertSame('1 YB', Number::fileSize(1024 ** 8)); - $this->assertSame('1,024 YB', Number::fileSize(1024 ** 9)); + return min(max($number, $min), $max); } - public function testClamp() + /** + * Split the given number into pairs of min/max values. + * + * @param int|float $to + * @param int|float $by + * @param int|float $offset + * @return array + */ + public static function pairs(int|float $to, int|float $by, int|float $offset = 1) { - $this->assertSame(2, Number::clamp(1, 2, 3)); - $this->assertSame(3, Number::clamp(5, 2, 3)); - $this->assertSame(5, Number::clamp(5, 1, 10)); - $this->assertSame(4.5, Number::clamp(4.5, 1, 10)); - $this->assertSame(1, Number::clamp(-10, 1, 5)); + $output = []; + + for ($lower = 0; $lower < $to; $lower += $by) { + $upper = $lower + $by; + + if ($upper > $to) { + $upper = $to; + } + + $output[] = [$lower + $offset, $upper]; + } + + return $output; } - public function testToHuman() + /** + * Remove any trailing zero digits after the decimal point of the given number. + * + * @param int|float $number + * @return int|float + */ + public static function trim(int|float $number) { - $this->assertSame('1', Number::forHumans(1)); - $this->assertSame('1.00', Number::forHumans(1, precision: 2)); - $this->assertSame('10', Number::forHumans(10)); - $this->assertSame('100', Number::forHumans(100)); - $this->assertSame('1 thousand', Number::forHumans(1000)); - $this->assertSame('1.00 thousand', Number::forHumans(1000, precision: 2)); - $this->assertSame('1 thousand', Number::forHumans(1000, maxPrecision: 2)); - $this->assertSame('1 thousand', Number::forHumans(1230)); - $this->assertSame('1.2 thousand', Number::forHumans(1230, maxPrecision: 1)); - $this->assertSame('1 million', Number::forHumans(1000000)); - $this->assertSame('1 billion', Number::forHumans(1000000000)); - $this->assertSame('1 trillion', Number::forHumans(1000000000000)); - $this->assertSame('1 quadrillion', Number::forHumans(1000000000000000)); - $this->assertSame('1 thousand quadrillion', Number::forHumans(1000000000000000000)); - - $this->assertSame('123', Number::forHumans(123)); - $this->assertSame('1 thousand', Number::forHumans(1234)); - $this->assertSame('1.23 thousand', Number::forHumans(1234, precision: 2)); - $this->assertSame('12 thousand', Number::forHumans(12345)); - $this->assertSame('1 million', Number::forHumans(1234567)); - $this->assertSame('1 billion', Number::forHumans(1234567890)); - $this->assertSame('1 trillion', Number::forHumans(1234567890123)); - $this->assertSame('1.23 trillion', Number::forHumans(1234567890123, precision: 2)); - $this->assertSame('1 quadrillion', Number::forHumans(1234567890123456)); - $this->assertSame('1.23 thousand quadrillion', Number::forHumans(1234567890123456789, precision: 2)); - $this->assertSame('490 thousand', Number::forHumans(489939)); - $this->assertSame('489.9390 thousand', Number::forHumans(489939, precision: 4)); - $this->assertSame('500.00000 million', Number::forHumans(500000000, precision: 5)); - - $this->assertSame('1 million quadrillion', Number::forHumans(1000000000000000000000)); - $this->assertSame('1 billion quadrillion', Number::forHumans(1000000000000000000000000)); - $this->assertSame('1 trillion quadrillion', Number::forHumans(1000000000000000000000000000)); - $this->assertSame('1 quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000)); - $this->assertSame('1 thousand quadrillion quadrillion', Number::forHumans(1000000000000000000000000000000000)); - - $this->assertSame('0', Number::forHumans(0)); - $this->assertSame('0', Number::forHumans(0.0)); - $this->assertSame('0.00', Number::forHumans(0, 2)); - $this->assertSame('0.00', Number::forHumans(0.0, 2)); - $this->assertSame('-1', Number::forHumans(-1)); - $this->assertSame('-1.00', Number::forHumans(-1, precision: 2)); - $this->assertSame('-10', Number::forHumans(-10)); - $this->assertSame('-100', Number::forHumans(-100)); - $this->assertSame('-1 thousand', Number::forHumans(-1000)); - $this->assertSame('-1.23 thousand', Number::forHumans(-1234, precision: 2)); - $this->assertSame('-1.2 thousand', Number::forHumans(-1234, maxPrecision: 1)); - $this->assertSame('-1 million', Number::forHumans(-1000000)); - $this->assertSame('-1 billion', Number::forHumans(-1000000000)); - $this->assertSame('-1 trillion', Number::forHumans(-1000000000000)); - $this->assertSame('-1.1 trillion', Number::forHumans(-1100000000000, maxPrecision: 1)); - $this->assertSame('-1 quadrillion', Number::forHumans(-1000000000000000)); - $this->assertSame('-1 thousand quadrillion', Number::forHumans(-1000000000000000000)); + return json_decode(json_encode($number)); } - public function testSummarize() + /** + * Execute the given callback using the given locale. + * + * @param string $locale + * @param callable $callback + * @return mixed + */ + public static function withLocale(string $locale, callable $callback) { - $this->assertSame('1', Number::abbreviate(1)); - $this->assertSame('1.00', Number::abbreviate(1, precision: 2)); - $this->assertSame('10', Number::abbreviate(10)); - $this->assertSame('100', Number::abbreviate(100)); - $this->assertSame('1K', Number::abbreviate(1000)); - $this->assertSame('1.00K', Number::abbreviate(1000, precision: 2)); - $this->assertSame('1K', Number::abbreviate(1000, maxPrecision: 2)); - $this->assertSame('1K', Number::abbreviate(1230)); - $this->assertSame('1.2K', Number::abbreviate(1230, maxPrecision: 1)); - $this->assertSame('1M', Number::abbreviate(1000000)); - $this->assertSame('1B', Number::abbreviate(1000000000)); - $this->assertSame('1T', Number::abbreviate(1000000000000)); - $this->assertSame('1Q', Number::abbreviate(1000000000000000)); - $this->assertSame('1KQ', Number::abbreviate(1000000000000000000)); - - $this->assertSame('123', Number::abbreviate(123)); - $this->assertSame('1K', Number::abbreviate(1234)); - $this->assertSame('1.23K', Number::abbreviate(1234, precision: 2)); - $this->assertSame('12K', Number::abbreviate(12345)); - $this->assertSame('1M', Number::abbreviate(1234567)); - $this->assertSame('1B', Number::abbreviate(1234567890)); - $this->assertSame('1T', Number::abbreviate(1234567890123)); - $this->assertSame('1.23T', Number::abbreviate(1234567890123, precision: 2)); - $this->assertSame('1Q', Number::abbreviate(1234567890123456)); - $this->assertSame('1.23KQ', Number::abbreviate(1234567890123456789, precision: 2)); - $this->assertSame('490K', Number::abbreviate(489939)); - $this->assertSame('489.9390K', Number::abbreviate(489939, precision: 4)); - $this->assertSame('500.00000M', Number::abbreviate(500000000, precision: 5)); - - $this->assertSame('1MQ', Number::abbreviate(1000000000000000000000)); - $this->assertSame('1BQ', Number::abbreviate(1000000000000000000000000)); - $this->assertSame('1TQ', Number::abbreviate(1000000000000000000000000000)); - $this->assertSame('1QQ', Number::abbreviate(1000000000000000000000000000000)); - $this->assertSame('1KQQ', Number::abbreviate(1000000000000000000000000000000000)); - - $this->assertSame('0', Number::abbreviate(0)); - $this->assertSame('0', Number::abbreviate(0.0)); - $this->assertSame('0.00', Number::abbreviate(0, 2)); - $this->assertSame('0.00', Number::abbreviate(0.0, 2)); - $this->assertSame('-1', Number::abbreviate(-1)); - $this->assertSame('-1.00', Number::abbreviate(-1, precision: 2)); - $this->assertSame('-10', Number::abbreviate(-10)); - $this->assertSame('-100', Number::abbreviate(-100)); - $this->assertSame('-1K', Number::abbreviate(-1000)); - $this->assertSame('-1.23K', Number::abbreviate(-1234, precision: 2)); - $this->assertSame('-1.2K', Number::abbreviate(-1234, maxPrecision: 1)); - $this->assertSame('-1M', Number::abbreviate(-1000000)); - $this->assertSame('-1B', Number::abbreviate(-1000000000)); - $this->assertSame('-1T', Number::abbreviate(-1000000000000)); - $this->assertSame('-1.1T', Number::abbreviate(-1100000000000, maxPrecision: 1)); - $this->assertSame('-1Q', Number::abbreviate(-1000000000000000)); - $this->assertSame('-1KQ', Number::abbreviate(-1000000000000000000)); + $previousLocale = static::$locale; + + static::useLocale($locale); + + return tap($callback(), fn () => static::useLocale($previousLocale)); } - public function testPairs() + /** + * Set the default locale. + * + * @param string $locale + * @return void + */ + public static function useLocale(string $locale) { - $this->assertSame([[1, 10], [11, 20], [21, 25]], Number::pairs(25, 10)); - $this->assertSame([[0, 10], [10, 20], [20, 25]], Number::pairs(25, 10, 0)); - $this->assertSame([[0, 2.5], [2.5, 5.0], [5.0, 7.5], [7.5, 10.0]], Number::pairs(10, 2.5, 0)); + static::$locale = $locale; } - public function testTrim() + /** + * Ensure the "intl" PHP extension is installed. + * + * @return void + */ + protected static function ensureIntlExtensionIsInstalled() { - $this->assertSame(12, Number::trim(12)); - $this->assertSame(120, Number::trim(120)); - $this->assertSame(12, Number::trim(12.0)); - $this->assertSame(12.3, Number::trim(12.3)); - $this->assertSame(12.3, Number::trim(12.30)); - $this->assertSame(12.3456789, Number::trim(12.3456789)); - $this->assertSame(12.3456789, Number::trim(12.34567890000)); + if (! extension_loaded('intl')) { + $method = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function']; + + throw new RuntimeException('The "intl" PHP extension is required to use the ['.$method.'] method.'); + } } } diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index fbf5eeff6d0d..9b996e470544 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -96,6 +96,21 @@ public function testStringApa() $this->assertSame('To Kill a Mockingbird', Str::apa('to kill a mockingbird')); $this->assertSame('To Kill a Mockingbird', Str::apa('TO KILL A MOCKINGBIRD')); $this->assertSame('To Kill a Mockingbird', Str::apa('To Kill A Mockingbird')); + + $this->assertSame('Être Écrivain Commence par Être un Lecteur.', Str::apa('Être écrivain commence par être un lecteur.')); + $this->assertSame('Être Écrivain Commence par Être un Lecteur.', Str::apa('Être Écrivain Commence par Être un Lecteur.')); + $this->assertSame('Être Écrivain Commence par Être un Lecteur.', Str::apa('ÊTRE ÉCRIVAIN COMMENCE PAR ÊTRE UN LECTEUR.')); + + $this->assertSame("C'est-à-Dire.", Str::apa("c'est-à-dire.")); + $this->assertSame("C'est-à-Dire.", Str::apa("C'est-à-Dire.")); + $this->assertSame("C'est-à-Dire.", Str::apa("C'EsT-À-DIRE.")); + + $this->assertSame('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.', Str::apa('устное слово – не воробей. как только он вылетит, его не поймаешь.')); + $this->assertSame('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.', Str::apa('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.')); + $this->assertSame('Устное Слово – Не Воробей. Как Только Он Вылетит, Его Не Поймаешь.', Str::apa('УСТНОЕ СЛОВО – НЕ ВОРОБЕЙ. КАК ТОЛЬКО ОН ВЫЛЕТИТ, ЕГО НЕ ПОЙМАЕШЬ.')); + + $this->assertSame('', Str::apa('')); + $this->assertSame(' ', Str::apa(' ')); } public function testStringWithoutWordsDoesntProduceError() @@ -425,6 +440,15 @@ public function testWrap() $this->assertEquals('foo-bar-baz', Str::wrap('-bar-', 'foo', 'baz')); } + public function testUnwrap() + { + $this->assertEquals('value', Str::unwrap('"value"', '"')); + $this->assertEquals('value', Str::unwrap('"value', '"')); + $this->assertEquals('value', Str::unwrap('value"', '"')); + $this->assertEquals('bar', Str::unwrap('foo-bar-baz', 'foo-', '-baz')); + $this->assertEquals('some: "json"', Str::unwrap('{some: "json"}', '{', '}')); + } + public function testIs() { $this->assertTrue(Str::is('/', '/')); @@ -1361,6 +1385,18 @@ public function testPasswordCreation() Str::of(Str::password())->contains(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) ); } + + public function testToBase64() + { + $this->assertSame(base64_encode('foo'), Str::toBase64('foo')); + $this->assertSame(base64_encode('foobar'), Str::toBase64('foobar')); + } + + public function testFromBase64() + { + $this->assertSame('foo', Str::fromBase64(base64_encode('foo'))); + $this->assertSame('foobar', Str::fromBase64(base64_encode('foobar'), true)); + } } class StringableObjectStub diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 5cf433fa4973..6165bee79944 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -1177,6 +1177,13 @@ public function testWrap() $this->assertEquals('"value"', $this->stringable('value')->wrap('"')); } + public function testUnwrap() + { + $this->assertEquals('value', $this->stringable('"value"')->unwrap('"')); + $this->assertEquals('bar', $this->stringable('foo-bar-baz')->unwrap('foo-', '-baz')); + $this->assertEquals('some: "json"', $this->stringable('{some: "json"}')->unwrap('{', '}')); + } + public function testToHtmlString() { $this->assertEquals( @@ -1295,4 +1302,18 @@ public function testArrayAccess() $this->assertTrue(isset($str[2])); $this->assertFalse(isset($str[10])); } + + public function testToBase64() + { + $this->assertSame(base64_encode('foo'), (string) $this->stringable('foo')->toBase64()); + $this->assertSame(base64_encode('foobar'), (string) $this->stringable('foobar')->toBase64()); + $this->assertSame(base64_encode('foobarbaz'), (string) $this->stringable('foobarbaz')->toBase64()); + } + + public function testFromBase64() + { + $this->assertSame('foo', (string) $this->stringable(base64_encode('foo'))->fromBase64()); + $this->assertSame('foobar', (string) $this->stringable(base64_encode('foobar'))->fromBase64(true)); + $this->assertSame('foobarbaz', (string) $this->stringable(base64_encode('foobarbaz'))->fromBase64()); + } } diff --git a/tests/Support/SupportTestingNotificationFakeTest.php b/tests/Support/SupportTestingNotificationFakeTest.php index 878296c18e42..10fce3dbde54 100644 --- a/tests/Support/SupportTestingNotificationFakeTest.php +++ b/tests/Support/SupportTestingNotificationFakeTest.php @@ -3,6 +3,8 @@ namespace Illuminate\Tests\Support; use Exception; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Foundation\Auth\User; use Illuminate\Notifications\AnonymousNotifiable; @@ -221,6 +223,16 @@ public function testAssertSentToWhenNotifiableHasFalsyShouldSend() $this->fake->assertNotSentTo($user, NotificationWithFalsyShouldSendStub::class); } + + public function testAssertItCanSerializeAndRestoreNotifications() + { + $this->fake->serializeAndRestore(); + $this->fake->send($this->user, new NotificationWithSerialization('hello')); + + $this->fake->assertSentTo($this->user, NotificationWithSerialization::class, function ($notification) { + return $notification->value === 'hello-serialized-unserialized'; + }); + } } class NotificationStub extends Notification @@ -256,3 +268,22 @@ public function preferredLocale() return 'au'; } } + +class NotificationWithSerialization extends NotificationStub implements ShouldQueue +{ + use Queueable; + + public function __construct(public $value) + { + } + + public function __serialize(): array + { + return ['value' => $this->value.'-serialized']; + } + + public function __unserialize(array $data): void + { + $this->value = $data['value'].'-unserialized'; + } +} diff --git a/tests/Support/ValidatedInputTest.php b/tests/Support/ValidatedInputTest.php index a8bdbe894322..6798bd69c914 100644 --- a/tests/Support/ValidatedInputTest.php +++ b/tests/Support/ValidatedInputTest.php @@ -2,7 +2,11 @@ namespace Illuminate\Tests\Support; +use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; +use Illuminate\Support\Stringable; use Illuminate\Support\ValidatedInput; +use Illuminate\Tests\Support\Fixtures\StringBackedEnum; use PHPUnit\Framework\TestCase; class ValidatedInputTest extends TestCase @@ -44,4 +48,481 @@ public function test_input_existence() $this->assertEquals(true, $inputB->has(['name', 'votes'])); } + + public function test_exists_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertTrue($input->exists('name')); + $this->assertTrue($input->exists('surname')); + $this->assertTrue($input->exists(['name', 'surname'])); + $this->assertTrue($input->exists('foo.bar')); + $this->assertTrue($input->exists(['name', 'foo.baz'])); + $this->assertTrue($input->exists(['name', 'foo'])); + $this->assertTrue($input->exists('foo')); + + $this->assertFalse($input->exists('votes')); + $this->assertFalse($input->exists(['name', 'votes'])); + $this->assertFalse($input->exists(['votes', 'foo.bar'])); + } + + public function test_has_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertTrue($input->has('name')); + $this->assertTrue($input->has('surname')); + $this->assertTrue($input->has(['name', 'surname'])); + $this->assertTrue($input->has('foo.bar')); + $this->assertTrue($input->has(['name', 'foo.baz'])); + $this->assertTrue($input->has(['name', 'foo'])); + $this->assertTrue($input->has('foo')); + + $this->assertFalse($input->has('votes')); + $this->assertFalse($input->has(['name', 'votes'])); + $this->assertFalse($input->has(['votes', 'foo.bar'])); + } + + public function test_has_any_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertTrue($input->hasAny('name')); + $this->assertTrue($input->hasAny('surname')); + $this->assertTrue($input->hasAny('foo.bar')); + $this->assertTrue($input->hasAny(['name', 'surname'])); + $this->assertTrue($input->hasAny(['name', 'foo.bat'])); + $this->assertTrue($input->hasAny(['votes', 'foo'])); + + $this->assertFalse($input->hasAny('votes')); + $this->assertFalse($input->hasAny(['votes', 'foo.bat'])); + } + + public function test_when_has_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'age' => '', 'foo' => ['bar' => null]]); + + $name = $age = $city = $foo = $bar = $baz = false; + + $input->whenHas('name', function ($value) use (&$name) { + $name = $value; + }); + + $input->whenHas('age', function ($value) use (&$age) { + $age = $value; + }); + + $input->whenHas('city', function ($value) use (&$city) { + $city = $value; + }); + + $input->whenHas('foo', function ($value) use (&$foo) { + $foo = $value; + }); + + $input->whenHas('foo.bar', function ($value) use (&$bar) { + $bar = $value; + }); + + $input->whenHas('foo.baz', function () use (&$baz) { + $baz = 'test'; + }, function () use (&$baz) { + $baz = true; + }); + + $this->assertSame('Fatih', $name); + $this->assertSame('', $age); + $this->assertFalse($city); + $this->assertEquals(['bar' => null], $foo); + $this->assertTrue($baz); + $this->assertNull($bar); + } + + public function test_filled_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertTrue($input->filled('name')); + $this->assertTrue($input->filled('surname')); + $this->assertTrue($input->filled(['name', 'surname'])); + $this->assertTrue($input->filled(['name', 'foo'])); + $this->assertTrue($input->filled('foo')); + + $this->assertFalse($input->filled('foo.bar')); + $this->assertFalse($input->filled(['name', 'foo.baz'])); + $this->assertFalse($input->filled('votes')); + $this->assertFalse($input->filled(['name', 'votes'])); + $this->assertFalse($input->filled(['votes', 'foo.bar'])); + } + + public function test_is_not_filled_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertFalse($input->isNotFilled('name')); + $this->assertFalse($input->isNotFilled('surname')); + $this->assertFalse($input->isNotFilled(['name', 'surname'])); + $this->assertFalse($input->isNotFilled(['name', 'foo'])); + $this->assertFalse($input->isNotFilled('foo')); + $this->assertFalse($input->isNotFilled(['name', 'foo.baz'])); + $this->assertFalse($input->isNotFilled(['name', 'votes'])); + + $this->assertTrue($input->isNotFilled('foo.bar')); + $this->assertTrue($input->isNotFilled('votes')); + $this->assertTrue($input->isNotFilled(['votes', 'foo.bar'])); + } + + public function test_any_filled_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertTrue($input->anyFilled('name')); + $this->assertTrue($input->anyFilled('surname')); + $this->assertTrue($input->anyFilled(['name', 'surname'])); + $this->assertTrue($input->anyFilled(['name', 'foo'])); + $this->assertTrue($input->anyFilled('foo')); + $this->assertTrue($input->anyFilled(['name', 'foo.baz'])); + $this->assertTrue($input->anyFilled(['name', 'votes'])); + + $this->assertFalse($input->anyFilled('foo.bar')); + $this->assertFalse($input->anyFilled('votes')); + $this->assertFalse($input->anyFilled(['votes', 'foo.bar'])); + } + + public function test_when_filled_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'age' => '', 'foo' => ['bar' => null]]); + + $name = $age = $city = $foo = $bar = $baz = false; + + $input->whenFilled('name', function ($value) use (&$name) { + $name = $value; + }); + + $input->whenFilled('age', function ($value) use (&$age) { + $age = $value; + }); + + $input->whenFilled('city', function ($value) use (&$city) { + $city = $value; + }); + + $input->whenFilled('foo', function ($value) use (&$foo) { + $foo = $value; + }); + + $input->whenFilled('foo.bar', function ($value) use (&$bar) { + $bar = $value; + }); + + $input->whenFilled('foo.baz', function () use (&$baz) { + $baz = 'test'; + }, function () use (&$baz) { + $baz = true; + }); + + $this->assertSame('Fatih', $name); + $this->assertEquals(['bar' => null], $foo); + $this->assertTrue($baz); + $this->assertFalse($age); + $this->assertFalse($city); + $this->assertFalse($bar); + } + + public function test_missing_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertFalse($input->missing('name')); + $this->assertFalse($input->missing('surname')); + $this->assertFalse($input->missing(['name', 'surname'])); + $this->assertFalse($input->missing('foo.bar')); + $this->assertFalse($input->missing(['name', 'foo.baz'])); + $this->assertFalse($input->missing(['name', 'foo'])); + $this->assertFalse($input->missing('foo')); + + $this->assertTrue($input->missing('votes')); + $this->assertTrue($input->missing(['name', 'votes'])); + $this->assertTrue($input->missing(['votes', 'foo.bar'])); + } + + public function test_when_missing_method() + { + $input = new ValidatedInput(['foo' => ['bar' => null]]); + + $name = $age = $city = $foo = $bar = $baz = false; + + $input->whenMissing('name', function () use (&$name) { + $name = 'Fatih'; + }); + + $input->whenMissing('age', function () use (&$age) { + $age = ''; + }); + + $input->whenMissing('city', function () use (&$city) { + $city = null; + }); + + $input->whenMissing('foo', function ($value) use (&$foo) { + $foo = $value; + }); + + $input->whenMissing('foo.baz', function () use (&$baz) { + $baz = true; + }); + + $input->whenMissing('foo.bar', function () use (&$bar) { + $bar = 'test'; + }, function () use (&$bar) { + $bar = true; + }); + + $this->assertSame('Fatih', $name); + $this->assertSame('', $age); + $this->assertNull($city); + $this->assertFalse($foo); + $this->assertTrue($baz); + $this->assertTrue($bar); + } + + public function test_keys_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertEquals(['name', 'surname', 'foo'], $input->keys()); + } + + public function test_all_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertEquals(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']], $input->all()); + } + + public function test_input_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertSame('Fatih', $input->input('name')); + $this->assertSame(null, $input->input('foo.bar')); + $this->assertSame('test', $input->input('foo.bat', 'test')); + } + + public function test_str_method() + { + $input = new ValidatedInput([ + 'int' => 123, + 'int_str' => '456', + 'float' => 123.456, + 'float_str' => '123.456', + 'float_zero' => 0.000, + 'float_str_zero' => '0.000', + 'str' => 'abc', + 'empty_str' => '', + 'null' => null, + ]); + + $this->assertTrue($input->str('int') instanceof Stringable); + $this->assertTrue($input->str('int') instanceof Stringable); + $this->assertTrue($input->str('unknown_key') instanceof Stringable); + $this->assertSame('123', $input->str('int')->value()); + $this->assertSame('456', $input->str('int_str')->value()); + $this->assertSame('123.456', $input->str('float')->value()); + $this->assertSame('123.456', $input->str('float_str')->value()); + $this->assertSame('0', $input->str('float_zero')->value()); + $this->assertSame('0.000', $input->str('float_str_zero')->value()); + $this->assertSame('', $input->str('empty_str')->value()); + $this->assertSame('', $input->str('null')->value()); + $this->assertSame('', $input->str('unknown_key')->value()); + } + + public function test_string_method() + { + $input = new ValidatedInput([ + 'int' => 123, + 'int_str' => '456', + 'float' => 123.456, + 'float_str' => '123.456', + 'float_zero' => 0.000, + 'float_str_zero' => '0.000', + 'str' => 'abc', + 'empty_str' => '', + 'null' => null, + ]); + + $this->assertTrue($input->string('int') instanceof Stringable); + $this->assertTrue($input->string('int') instanceof Stringable); + $this->assertTrue($input->string('unknown_key') instanceof Stringable); + $this->assertSame('123', $input->string('int')->value()); + $this->assertSame('456', $input->string('int_str')->value()); + $this->assertSame('123.456', $input->string('float')->value()); + $this->assertSame('123.456', $input->string('float_str')->value()); + $this->assertSame('0', $input->string('float_zero')->value()); + $this->assertSame('0.000', $input->string('float_str_zero')->value()); + $this->assertSame('', $input->string('empty_str')->value()); + $this->assertSame('', $input->string('null')->value()); + $this->assertSame('', $input->string('unknown_key')->value()); + } + + public function test_boolean_method() + { + $input = new ValidatedInput([ + 'with_trashed' => 'false', + 'download' => true, + 'checked' => 1, + 'unchecked' => '0', + 'with_on' => 'on', + 'with_yes' => 'yes', + ]); + + $this->assertTrue($input->boolean('checked')); + $this->assertTrue($input->boolean('download')); + $this->assertFalse($input->boolean('unchecked')); + $this->assertFalse($input->boolean('with_trashed')); + $this->assertFalse($input->boolean('some_undefined_key')); + $this->assertTrue($input->boolean('with_on')); + $this->assertTrue($input->boolean('with_yes')); + } + + public function test_integer_method() + { + $input = new ValidatedInput([ + 'int' => '123', + 'raw_int' => 456, + 'zero_padded' => '078', + 'space_padded' => ' 901', + 'nan' => 'nan', + 'mixed' => '1ab', + 'underscore_notation' => '2_000', + 'null' => null, + ]); + + $this->assertSame(123, $input->integer('int')); + $this->assertSame(456, $input->integer('raw_int')); + $this->assertSame(78, $input->integer('zero_padded')); + $this->assertSame(901, $input->integer('space_padded')); + $this->assertSame(0, $input->integer('nan')); + $this->assertSame(1, $input->integer('mixed')); + $this->assertSame(2, $input->integer('underscore_notation')); + $this->assertSame(123456, $input->integer('unknown_key', 123456)); + $this->assertSame(0, $input->integer('null')); + $this->assertSame(0, $input->integer('null', 123456)); + } + + public function test_float_method() + { + $input = new ValidatedInput([ + 'float' => '1.23', + 'raw_float' => 45.6, + 'decimal_only' => '.6', + 'zero_padded' => '0.78', + 'space_padded' => ' 90.1', + 'nan' => 'nan', + 'mixed' => '1.ab', + 'scientific_notation' => '1e3', + 'null' => null, + ]); + + $this->assertSame(1.23, $input->float('float')); + $this->assertSame(45.6, $input->float('raw_float')); + $this->assertSame(.6, $input->float('decimal_only')); + $this->assertSame(0.78, $input->float('zero_padded')); + $this->assertSame(90.1, $input->float('space_padded')); + $this->assertSame(0.0, $input->float('nan')); + $this->assertSame(1.0, $input->float('mixed')); + $this->assertSame(1e3, $input->float('scientific_notation')); + $this->assertSame(123.456, $input->float('unknown_key', 123.456)); + $this->assertSame(0.0, $input->float('null')); + $this->assertSame(0.0, $input->float('null', 123.456)); + } + + public function test_date_method() + { + $input = new ValidatedInput([ + 'as_null' => null, + 'as_invalid' => 'invalid', + + 'as_datetime' => '24-01-01 16:30:25', + 'as_format' => '1704126625', + 'as_timezone' => '24-01-01 13:30:25', + + 'as_date' => '2024-01-01', + 'as_time' => '16:30:25', + ]); + + $current = Carbon::create(2024, 1, 1, 16, 30, 25); + + $this->assertNull($input->date('as_null')); + $this->assertNull($input->date('doesnt_exists')); + + $this->assertEquals($current, $input->date('as_datetime')); + $this->assertEquals($current->format('Y-m-d H:i:s P'), $input->date('as_format', 'U')->format('Y-m-d H:i:s P')); + $this->assertEquals($current, $input->date('as_timezone', null, 'America/Santiago')); + + $this->assertTrue($input->date('as_date')->isSameDay($current)); + $this->assertTrue($input->date('as_time')->isSameSecond('16:30:25')); + } + + public function test_enum_method() + { + $input = new ValidatedInput([ + 'valid_enum_value' => 'Hello world', + 'invalid_enum_value' => 'invalid', + ]); + + $this->assertNull($input->enum('doesnt_exists', StringBackedEnum::class)); + + $this->assertEquals(StringBackedEnum::HELLO_WORLD, $input->enum('valid_enum_value', StringBackedEnum::class)); + + $this->assertNull($input->enum('invalid_enum_value', StringBackedEnum::class)); + } + + public function test_collect_method() + { + $input = new ValidatedInput(['users' => [1, 2, 3]]); + + $this->assertInstanceOf(Collection::class, $input->collect('users')); + $this->assertTrue($input->collect('developers')->isEmpty()); + $this->assertEquals([1, 2, 3], $input->collect('users')->all()); + $this->assertEquals(['users' => [1, 2, 3]], $input->collect()->all()); + + $input = new ValidatedInput(['text-payload']); + $this->assertEquals(['text-payload'], $input->collect()->all()); + + $input = new ValidatedInput(['email' => 'test@example.com']); + $this->assertEquals(['test@example.com'], $input->collect('email')->all()); + + $input = new ValidatedInput([]); + $this->assertInstanceOf(Collection::class, $input->collect()); + $this->assertTrue($input->collect()->isEmpty()); + + $input = new ValidatedInput(['users' => [1, 2, 3], 'roles' => [4, 5, 6], 'foo' => ['bar', 'baz'], 'email' => 'test@example.com']); + $this->assertInstanceOf(Collection::class, $input->collect(['users'])); + $this->assertTrue($input->collect(['developers'])->isEmpty()); + $this->assertTrue($input->collect(['roles'])->isNotEmpty()); + $this->assertEquals(['roles' => [4, 5, 6]], $input->collect(['roles'])->all()); + $this->assertEquals(['users' => [1, 2, 3], 'email' => 'test@example.com'], $input->collect(['users', 'email'])->all()); + $this->assertEquals(collect(['roles' => [4, 5, 6], 'foo' => ['bar', 'baz']]), $input->collect(['roles', 'foo'])); + $this->assertEquals(['users' => [1, 2, 3], 'roles' => [4, 5, 6], 'foo' => ['bar', 'baz'], 'email' => 'test@example.com'], $input->collect()->all()); + } + + public function test_only_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertEquals(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null]], $input->only('name', 'surname', 'foo.bar')); + $this->assertEquals(['name' => 'Fatih', 'foo' => ['bar' => null, 'baz' => '']], $input->only('name', 'foo')); + $this->assertEquals(['foo' => ['baz' => '']], $input->only('foo.baz')); + $this->assertEquals(['name' => 'Fatih'], $input->only('name')); + } + + public function test_except_method() + { + $input = new ValidatedInput(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null, 'baz' => '']]); + + $this->assertEquals(['name' => 'Fatih', 'surname' => 'AYDIN', 'foo' => ['bar' => null]], $input->except('foo.baz')); + $this->assertEquals(['surname' => 'AYDIN'], $input->except('name', 'foo')); + $this->assertEquals([], $input->except('name', 'surname', 'foo')); + } } diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 3934bbbebc13..f4c1ee713a6b 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -28,6 +28,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; use Symfony\Component\HttpFoundation\StreamedResponse; class TestResponseTest extends TestCase @@ -285,6 +286,49 @@ public function testAssertStreamedContent() } } + public function testAssertStreamedJsonContent() + { + $response = TestResponse::fromBaseResponse( + new StreamedJsonResponse([ + 'data' => $this->yieldTestModels(), + ]) + ); + + $response->assertStreamedJsonContent([ + 'data' => [ + ['id' => 1], + ['id' => 2], + ['id' => 3], + ], + ]); + + try { + $response->assertStreamedJsonContent([ + 'data' => [ + ['id' => 1], + ['id' => 2], + ], + ]); + $this->fail('xxxx'); + } catch (AssertionFailedError $e) { + $this->assertSame('Failed asserting that two strings are identical.', $e->getMessage()); + } + + try { + $response->assertStreamedContent('not expected response string'); + $this->fail('xxxx'); + } catch (AssertionFailedError $e) { + $this->assertSame('Failed asserting that two strings are identical.', $e->getMessage()); + } + } + + public function yieldTestModels() + { + yield new TestModel(['id' => 1]); + yield new TestModel(['id' => 2]); + yield new TestModel(['id' => 3]); + } + public function testAssertSee() { $response = $this->makeMockResponse([ diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php index 8601f1493ea7..07ec429aab77 100755 --- a/tests/Translation/TranslationTranslatorTest.php +++ b/tests/Translation/TranslationTranslatorTest.php @@ -123,7 +123,7 @@ public function testGetMethodProperlyLoadsAndRetrievesItemForGlobalNamespace() $this->assertSame('breeze bar', $t->get('foo.bar', ['foo' => 'bar'])); } - public function testChoiceMethodProperlyLoadsAndRetrievesItem() + public function testChoiceMethodProperlyLoadsAndRetrievesItemForAnInt() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); @@ -133,6 +133,16 @@ public function testChoiceMethodProperlyLoadsAndRetrievesItem() $t->choice('foo', 10, ['replace']); } + public function testChoiceMethodProperlyLoadsAndRetrievesItemForAFloat() + { + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->setSelector($selector = m::mock(MessageSelector::class)); + $selector->shouldReceive('choose')->once()->with('line', 1.2, 'en')->andReturn('choiced'); + + $t->choice('foo', 1.2, ['replace']); + } + public function testChoiceMethodProperlyCountsCollectionsAndLoadsAndRetrievesItem() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); diff --git a/tests/Validation/ValidationEnumRuleTest.php b/tests/Validation/ValidationEnumRuleTest.php index 1c14c766f262..beffc1b314b1 100644 --- a/tests/Validation/ValidationEnumRuleTest.php +++ b/tests/Validation/ValidationEnumRuleTest.php @@ -78,6 +78,84 @@ public function testValidationFailsWhenProvidingNoExistingCases() $this->assertEquals(['The selected status is invalid.'], $v->messages()->get('status')); } + public function testValidationPassesForAllCasesUntilEitherOnlyOrExceptIsPassed() + { + $v = new Validator( + resolve('translator'), + [ + 'status_1' => PureEnum::one, + 'status_2' => PureEnum::two, + 'status_3' => IntegerStatus::done->value, + ], + [ + 'status_1' => new Enum(PureEnum::class), + 'status_2' => (new Enum(PureEnum::class))->only([])->except([]), + 'status_3' => new Enum(IntegerStatus::class), + ], + ); + + $this->assertTrue($v->passes()); + } + + /** + * @dataProvider conditionalCasesDataProvider + */ + public function testValidationPassesWhenOnlyCasesProvided( + IntegerStatus|int $enum, + array|IntegerStatus $only, + bool $expected + ) { + $v = new Validator( + resolve('translator'), + [ + 'status' => $enum, + ], + [ + 'status' => (new Enum(IntegerStatus::class))->only($only), + ], + ); + + $this->assertSame($expected, $v->passes()); + } + + /** + * @dataProvider conditionalCasesDataProvider + */ + public function testValidationPassesWhenExceptCasesProvided( + int|IntegerStatus $enum, + array|IntegerStatus $except, + bool $expected + ) { + $v = new Validator( + resolve('translator'), + [ + 'status' => $enum, + ], + [ + 'status' => (new Enum(IntegerStatus::class))->except($except), + ], + ); + + $this->assertSame($expected, $v->fails()); + } + + public function testOnlyHasHigherOrderThanExcept() + { + $v = new Validator( + resolve('translator'), + [ + 'status' => PureEnum::one, + ], + [ + 'status' => (new Enum(PureEnum::class)) + ->only(PureEnum::one) + ->except(PureEnum::one), + ], + ); + + $this->assertTrue($v->passes()); + } + public function testValidationFailsWhenProvidingDifferentType() { $v = new Validator( @@ -171,6 +249,16 @@ public function testValidationFailsWhenProvidingStringToIntegerType() $this->assertEquals(['The selected status is invalid.'], $v->messages()->get('status')); } + public static function conditionalCasesDataProvider(): array + { + return [ + [IntegerStatus::done, IntegerStatus::done, true], + [IntegerStatus::done, [IntegerStatus::done, IntegerStatus::pending], true], + [IntegerStatus::pending->value, [IntegerStatus::done, IntegerStatus::pending], true], + [IntegerStatus::done->value, IntegerStatus::pending, false], + ]; + } + protected function setUp(): void { $container = Container::getInstance(); diff --git a/tests/Validation/ValidationFileRuleTest.php b/tests/Validation/ValidationFileRuleTest.php index 838aa6615471..1705a3ef198b 100644 --- a/tests/Validation/ValidationFileRuleTest.php +++ b/tests/Validation/ValidationFileRuleTest.php @@ -344,6 +344,21 @@ public function testMacro() ); } + public function testItUsesTheCorrectValidationMessageForFile(): void + { + file_put_contents($path = __DIR__.'/test.json', 'this-is-a-test'); + + $file = new \Illuminate\Http\File($path); + + $this->fails( + ['max:0'], + $file, + ['validation.max.file'] + ); + + unlink($path); + } + public function testItCanSetDefaultUsing() { $this->assertInstanceOf(File::class, File::default()); diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index 3cfc7c43911a..8fb9673ebff9 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -42,6 +42,15 @@ public function testMin() $this->passes(new Password(8), ['88888888']); } + public function testMax() + { + $this->fails(Password::min(2)->max(4), ['aaaaa', '11111111'], [ + 'validation.max.string', + ]); + + $this->passes(Password::min(2)->max(3), ['aa', '111']); + } + public function testConditional() { $is_privileged_user = true; diff --git a/tests/Validation/ValidationRequiredIfTest.php b/tests/Validation/ValidationRequiredIfTest.php index 334ba2ea03ce..f0122fb3a02d 100644 --- a/tests/Validation/ValidationRequiredIfTest.php +++ b/tests/Validation/ValidationRequiredIfTest.php @@ -9,7 +9,7 @@ class ValidationRequiredIfTest extends TestCase { - public function testItClousureReturnsFormatsAStringVersionOfTheRule() + public function testItClosureReturnsFormatsAStringVersionOfTheRule() { $rule = new RequiredIf(function () { return true; diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php index 2ead8eecbb9d..367d8b59c24f 100644 --- a/tests/Validation/ValidationRuleParserTest.php +++ b/tests/Validation/ValidationRuleParserTest.php @@ -28,6 +28,10 @@ public function testConditionalRulesAreProperlyExpandedAndFiltered() 'zip' => ['required', Rule::when($isAdmin, function (Fluent $input) { return ['min:2']; })], + 'when_cb_true' => Rule::when(fn () => true, ['required'], ['nullable']), + 'when_cb_false' => Rule::when(fn () => false, ['required'], ['nullable']), + 'unless_cb_true' => Rule::unless(fn () => true, ['required'], ['nullable']), + 'unless_cb_false' => Rule::unless(fn () => false, ['required'], ['nullable']), ]); $this->assertEquals([ @@ -39,6 +43,10 @@ public function testConditionalRulesAreProperlyExpandedAndFiltered() 'city' => ['required', 'min:2'], 'state' => ['required', 'min:2'], 'zip' => ['required', 'min:2'], + 'when_cb_true' => ['required'], + 'when_cb_false' => ['nullable'], + 'unless_cb_true' => ['nullable'], + 'unless_cb_false' => ['required'], ], $rules); } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 7c9d7b31d3a2..bd3e5f834df2 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -293,6 +293,28 @@ public function testNullable() $this->assertSame('validation.boolean', $v->messages()->get('b')[0]); } + public function testArrayNullableWithUnvalidatedArrayKeys() + { + $trans = $this->getIlluminateArrayTranslator(); + + $v = new Validator($trans, [ + 'x' => null, + ], [ + 'x' => 'array|nullable', + 'x.key' => 'string', + ]); + $this->assertTrue($v->passes()); + $this->assertArrayHasKey('x', $v->validated()); + + $v = new Validator($trans, [ + 'x' => null, + ], [ + 'x' => 'array', + 'x.key' => 'string', + ]); + $this->assertFalse($v->passes()); + } + public function testNullableMakesNoDifferenceIfImplicitRuleExists() { $trans = $this->getIlluminateArrayTranslator(); diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index a5ff36d82531..87e716edcee7 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Facade; use Illuminate\Support\HtmlString; use Illuminate\View\Component; +use Illuminate\View\ComponentSlot; use Illuminate\View\Factory; use Illuminate\View\View; use Mockery as m; @@ -305,6 +306,63 @@ public function testFactoryGetsSharedBetweenComponents() Component::forgetFactory(); $this->assertNotSame($this->viewFactory, $getFactory($inline)); } + + public function testComponentSlotIsEmpty() + { + $slot = new ComponentSlot(); + + $this->assertTrue((bool) $slot->isEmpty()); + } + + public function testComponentSlotSanitizedEmpty() + { + // default sanitizer should remove all html tags + $slot = new ComponentSlot(''); + + $linebreakingSlot = new ComponentSlot("\n \t"); + + $moreComplexSlot = new ComponentSlot(''); + + $this->assertFalse((bool) $slot->hasActualContent()); + $this->assertFalse((bool) $linebreakingSlot->hasActualContent('trim')); + $this->assertFalse((bool) $moreComplexSlot->hasActualContent()); + } + + public function testComponentSlotSanitizedNotEmpty() + { + // default sanitizer should remove all html tags + $slot = new ComponentSlot('not empty'); + + $linebreakingSlot = new ComponentSlot("\ntest \t"); + + $moreComplexSlot = new ComponentSlot('beforeafter'); + + $this->assertTrue((bool) $slot->hasActualContent()); + $this->assertTrue((bool) $linebreakingSlot->hasActualContent('trim')); + $this->assertTrue((bool) $moreComplexSlot->hasActualContent()); + } + + public function testComponentSlotIsNotEmpty() + { + $slot = new ComponentSlot('test'); + + $anotherSlot = new ComponentSlot('test'); + + $moreComplexSlot = new ComponentSlot('test'); + + $this->assertTrue((bool) $slot->hasActualContent()); + $this->assertTrue((bool) $anotherSlot->hasActualContent()); + $this->assertTrue((bool) $moreComplexSlot->hasActualContent()); + } } class TestInlineViewComponent extends Component diff --git a/types/Support/Collection.php b/types/Support/Collection.php index 77861041deaf..2e8b4a926386 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -657,6 +657,7 @@ function ($collection, $count) { assertType('Illuminate\Support\Collection', $collection->make([1])->concat([2])); assertType('Illuminate\Support\Collection', $collection->make(['string'])->concat(['string'])); +assertType('Illuminate\Support\Collection', $collection->make([1])->concat(['string'])); assertType('Illuminate\Support\Collection|int', $collection->make([1])->random(2)); assertType('Illuminate\Support\Collection|string', $collection->make(['string'])->random()); diff --git a/types/Support/Helpers.php b/types/Support/Helpers.php index 92257485d96c..b91b648e3f3d 100644 --- a/types/Support/Helpers.php +++ b/types/Support/Helpers.php @@ -32,7 +32,7 @@ return 10; })); -assertType('mixed', with(new User(), function ($user) { +assertType('User', with(new User(), function ($user) { return $user; })); assertType('User', with(new User(), function ($user): User { diff --git a/types/Support/LazyCollection.php b/types/Support/LazyCollection.php index c9ad444ec154..ef3c5f21578a 100644 --- a/types/Support/LazyCollection.php +++ b/types/Support/LazyCollection.php @@ -547,6 +547,7 @@ assertType('Illuminate\Support\LazyCollection', $collection->make([1])->concat([2])); assertType('Illuminate\Support\LazyCollection', $collection->make(['string'])->concat(['string'])); +assertType('Illuminate\Support\LazyCollection', $collection->make([1])->concat(['string'])); assertType('Illuminate\Support\LazyCollection|int', $collection->make([1])->random(2)); assertType('Illuminate\Support\LazyCollection|string', $collection->make(['string'])->random());