diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..e89cb3511502 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +/src/Illuminate/Collections/ @JosephSilber +/tests/Support/SupportCollectionTest/ @JosephSilber +/tests/Support/SupportLazyCollectionTest/ @JosephSilber +/tests/Support/SupportLazyCollectionIsLazyTest/ @JosephSilber diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 444b8e623703..1ce441d0affb 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,6 +6,8 @@ Version | Security Fixes Until --- | --- +8 | September 8th, 2021 +7 | March 3rd, 2021 6 (LTS) | September 3rd, 2022 5.8 | February 26th, 2020 5.7 | September 4th, 2019 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f880d3b5ae62..08ebae258f9c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: jobs: linux_tests: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 services: memcached: @@ -31,8 +31,13 @@ jobs: strategy: fail-fast: true matrix: - php: [7.3, 7.4, 8.0] + php: ['7.3', '7.4', '8.0'] stability: [prefer-lowest, prefer-stable] + include: + - php: '8.1' + flags: "--ignore-platform-req=php" + stability: prefer-stable + name: PHP ${{ matrix.php }} - ${{ matrix.stability }} @@ -48,9 +53,6 @@ jobs: tools: composer:v2 coverage: none - - name: Setup problem matchers - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Set Minimum Guzzle Version uses: nick-invision/retry@v1 with: @@ -64,13 +66,23 @@ jobs: with: timeout_minutes: 5 max_attempts: 5 - command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }} + + - name: Setup DynamoDB Local + uses: rrainn/dynamodb-action@v2.0.0 + with: + port: 8888 - name: Execute tests + continue-on-error: ${{ matrix.php > 8 }} run: vendor/bin/phpunit --verbose env: DB_PORT: ${{ job.services.mysql.ports[3306] }} DB_USERNAME: root + DYNAMODB_CACHE_TABLE: laravel_dynamodb_test + DYNAMODB_ENDPOINT: "http://localhost:8888" + AWS_ACCESS_KEY_ID: random_key + AWS_SECRET_ACCESS_KEY: random_secret windows_tests: runs-on: windows-latest @@ -78,8 +90,12 @@ jobs: strategy: fail-fast: true matrix: - php: [7.3, 7.4, 8.0] + php: ['7.3', '7.4', '8.0'] stability: [prefer-lowest, prefer-stable] + include: + - php: '8.1' + flags: "--ignore-platform-req=php" + stability: prefer-stable name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - Windows @@ -100,9 +116,6 @@ jobs: tools: composer:v2 coverage: none - - name: Setup problem matchers - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Set Minimum Guzzle Version uses: nick-invision/retry@v1 with: @@ -116,7 +129,8 @@ jobs: with: timeout_minutes: 5 max_attempts: 5 - command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }} - name: Execute tests + continue-on-error: ${{ matrix.php > 8 }} run: vendor/bin/phpunit --verbose diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index 8ec4a08e9afe..cc073b4d93fb 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,96 @@ # Release Notes for 6.x -## [Unreleased](https://github.com/laravel/framework/compare/v6.20.7...6.x) +## [Unreleased](https://github.com/laravel/framework/compare/v6.20.20...6.x) + + +## [v6.20.20 (2021-03-23)](https://github.com/laravel/framework/compare/v6.20.19...v6.20.20) + +### Added +- Added WSREP communication link failure for lost connection detection ([#36668](https://github.com/laravel/framework/pull/36668)) + +### Fixed +- Fixes the issue using cache:clear with PhpRedis and a clustered Redis instance. ([#36665](https://github.com/laravel/framework/pull/36665)) + + +## [v6.20.19 (2021-03-16)](https://github.com/laravel/framework/compare/v6.20.18...v6.20.19) + +### Added +- Added broken pipe exception as lost connection error ([#36601](https://github.com/laravel/framework/pull/36601)) + + +## [v6.20.18 (2021-03-09)](https://github.com/laravel/framework/compare/v6.20.17...v6.20.18) + +### Fixed +- Fix validator treating null as true for (required|exclude)_(if|unless) due to loose `in_array()` check ([#36504](https://github.com/laravel/framework/pull/36504)) + +### Changed +- Delete existing links that are broken in `Illuminate\Foundation\Console\StorageLinkCommand` ([#36470](https://github.com/laravel/framework/pull/36470)) + + +## [v6.20.17 (2021-03-02)](https://github.com/laravel/framework/compare/v6.20.16...v6.20.17) + +### Added +- Added new line to `DetectsLostConnections` ([#36373](https://github.com/laravel/framework/pull/36373)) + + +## [v6.20.16 (2021-02-02)](https://github.com/laravel/framework/compare/v6.20.15...v6.20.16) + +### Fixed +- Fixed `Illuminate\View\ViewException::report()` ([#36110](https://github.com/laravel/framework/pull/36110)) +- Fixed `Illuminate\Redis\Connections\PhpRedisConnection::spop()` ([#36106](https://github.com/laravel/framework/pull/36106)) + +### Changed +- Typecast page number as integer in `Illuminate\Pagination\AbstractPaginator::resolveCurrentPage()` ([#36055](https://github.com/laravel/framework/pull/36055)) + + +## [v6.20.15 (2021-01-26)](https://github.com/laravel/framework/compare/v6.20.14...v6.20.15) + +### Changed +- Pipe new through render and report exception methods ([#36037](https://github.com/laravel/framework/pull/36037)) + + +## [v6.20.14 (2021-01-21)](https://github.com/laravel/framework/compare/v6.20.13...v6.20.14) + +### Fixed +- Fixed type error in `Illuminate\Http\Concerns\InteractsWithContentTypes::isJson()` ([#35956](https://github.com/laravel/framework/pull/35956)) +- Limit expected bindings ([#35972](https://github.com/laravel/framework/pull/35972), [006873d](https://github.com/laravel/framework/commit/006873df411d28bfd03fea5e7f91a2afe3918498)) + + +## [v6.20.13 (2021-01-19)](https://github.com/laravel/framework/compare/v6.20.12...v6.20.13) + +### Fixed +- Fixed empty html mail ([#35941](https://github.com/laravel/framework/pull/35941)) + + +## [v6.20.12 (2021-01-13)](https://github.com/laravel/framework/compare/v6.20.11...v6.20.12) + + +## [v6.20.11 (2021-01-13)](https://github.com/laravel/framework/compare/v6.20.10...v6.20.11) + +### Fixed +- Limit expected bindings ([#35865](https://github.com/laravel/framework/pull/35865)) + + +## [v6.20.10 (2021-01-12)](https://github.com/laravel/framework/compare/v6.20.9...v6.20.10) + +### Added +- Added new line to `DetectsLostConnections` ([#35790](https://github.com/laravel/framework/pull/35790)) + +### Fixed +- Fixed error from missing null check on PHP 8 in `Illuminate\Validation\Concerns\ValidatesAttributes::validateJson()` ([#35797](https://github.com/laravel/framework/pull/35797)) + + +## [v6.20.9 (2021-01-05)](https://github.com/laravel/framework/compare/v6.20.8...v6.20.9) + +### Added +- [Updated Illuminate\Database\DetectsLostConnections with new strings](https://github.com/laravel/framework/compare/v6.20.8...v6.20.9) + + +## [v6.20.8 (2020-12-22)](https://github.com/laravel/framework/compare/v6.20.7...v6.20.8) + +### Fixed +- Fixed `Illuminate\Validation\Concerns\ValidatesAttributes::validateJson()` for PHP8 ([#35646](https://github.com/laravel/framework/pull/35646)) +- Catch DecryptException with invalid X-XSRF-TOKEN in `Illuminate\Foundation\Http\Middleware\VerifyCsrfToken` ([#35671](https://github.com/laravel/framework/pull/35671)) ## [v6.20.7 (2020-12-08)](https://github.com/laravel/framework/compare/v6.20.6...v6.20.7) @@ -193,6 +283,8 @@ ### Changed - Improve cookie encryption ([#33662](https://github.com/laravel/framework/pull/33662)) +This change will invalidate all existing cookies. Please see [this security bulletin](https://blog.laravel.com/laravel-cookie-security-releases) for more information. + ## [v6.18.26 (2020-07-21)](https://github.com/laravel/framework/compare/v6.18.25...v6.18.26) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index 12793d5c2139..dd2fae605a22 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,359 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.19.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.34.0...8.x) + + +## [v8.34.0 (2021-03-23)](https://github.com/laravel/framework/compare/v8.33.1...v8.34.0) + +### Inspiring +- Added more inspiring quotes ([92b7bde](https://github.com/laravel/framework/commit/92b7bdeb4b8c40848fa276cfe1897c656302942f)) + +### Added +- Added WSREP communication link failure for lost connection detection ([#36668](https://github.com/laravel/framework/pull/36668)) +- Added "except-path" option to `route:list` command ([#36619](https://github.com/laravel/framework/pull/36619), [76e11ee](https://github.com/laravel/framework/commit/76e11ee97fc8068be1d55986b4524d4c329af387)) +- Added `Illuminate\Support\Str::remove()` and `Illuminate\Support\Stringable::remove()` methods ([#36639](https://github.com/laravel/framework/pull/36639), [7b0259f](https://github.com/laravel/framework/commit/7b0259faa46409513b75a8a0b512b3aacfcad944), [20e2470](https://github.com/laravel/framework/commit/20e24701e71f71a44b477b4311d0cb69f97906f1)) +- Added `Illuminate\Database\Eloquent\Relations\MorphPivot::getMorphType()` ([#36640](https://github.com/laravel/framework/pull/36640), [7e08215](https://github.com/laravel/framework/commit/7e08215f0d370c3c33beb7bba7e2c1ee2ac7aab5)) +- Added assertion to verify type of key in JSON ([#36638](https://github.com/laravel/framework/pull/36638)) +- Added prohibited validation rule ([#36667](https://github.com/laravel/framework/pull/36667)) +- Added strict comparison to distinct validation rule ([#36669](https://github.com/laravel/framework/pull/36669)) +- Added `Illuminate\Translation\FileLoader::getJsonPaths()` ([#36689](https://github.com/laravel/framework/pull/36689)) +- Added `Illuminate\Support\Testing\Fakes\EventFake::assertAttached()` ([#36690](https://github.com/laravel/framework/pull/36690)) +- Added `lazy()` and `lazyById()` methods to `Illuminate\Database\Concerns\BuildsQueries` ([#36699](https://github.com/laravel/framework/pull/36699)) + +### Fixed +- Fixes the issue using cache:clear with PhpRedis and a clustered Redis instance. ([#36665](https://github.com/laravel/framework/pull/36665)) +- Fix replacing required :input with null on PHP 8.1 in `Illuminate\Validation\Concerns\FormatsMessages::getDisplayableValue()` ([#36622](https://github.com/laravel/framework/pull/36622)) +- Fixed artisan schema:dump error ([#36698](https://github.com/laravel/framework/pull/36698)) + +### Changed +- Adjust Fluent Assertions ([#36620](https://github.com/laravel/framework/pull/36620)) +- Added timestamp reference to schedule:work artisan command output ([#36621](https://github.com/laravel/framework/pull/36621)) +- Expect custom markdown mailable themes to be in mail subdirectory ([#36673](https://github.com/laravel/framework/pull/36673)) +- Throw exception when unable to create LockableFile ([#36674](https://github.com/laravel/framework/pull/36674)) + +### Refactoring +- Always prefer typesafe string comparisons ([#36657](https://github.com/laravel/framework/pull/36657)) + + +## [v8.33.1 (2021-03-16)](https://github.com/laravel/framework/compare/v8.33.0...v8.33.1) + +### Added +- Added `Illuminate\Database\Connection::forgetRecordModificationState()` ([#36617](https://github.com/laravel/framework/pull/36617)) + +### Reverted +- Reverted "Container - detect circular dependencies" ([332844e](https://github.com/laravel/framework/commit/332844e5bde34f8db91aeca4d21cd4e0925d691e)) + + +## [v8.33.0 (2021-03-16)](https://github.com/laravel/framework/compare/v8.32.1...v8.33.0) + +### Added +- Added broken pipe exception as lost connection error ([#36601](https://github.com/laravel/framework/pull/36601)) +- Added missing option to resource ([#36562](https://github.com/laravel/framework/pull/36562)) +- Introduce StringEncrypter interface ([#36578](https://github.com/laravel/framework/pull/36578)) + +### Fixed +- Fixed returns with Mail & Notification components ([#36559](https://github.com/laravel/framework/pull/36559)) +- Stack driver fix: respect the defined processors in LogManager ([#36591](https://github.com/laravel/framework/pull/36591)) +- Require the correct password to rehash it when logging out other devices ([#36608](https://github.com/laravel/framework/pull/36608), [1e61612](https://github.com/laravel/framework/commit/1e6161250074b8106c1fcf153eeaef7c0bf74c6c)) + +### Changed +- Allow nullable columns for `AsArrayObject/AsCollection` casts ([#36526](https://github.com/laravel/framework/pull/36526)) +- Accept callable class for reportable and renderable in exception handler ([#36551](https://github.com/laravel/framework/pull/36551)) +- Container - detect circular dependencies ([dd7274d](https://github.com/laravel/framework/commit/dd7274d23a9ee58cc1abdf7107403169a3994b68), [a712f72](https://github.com/laravel/framework/commit/a712f72ca88f709335576530b31635738abd4c89), [6f9bb4c](https://github.com/laravel/framework/commit/6f9bb4cdd84295cbcf7908cc4b4684f47f38b8cf)) +- Initialize CronExpression class using new keyword ([#36600](https://github.com/laravel/framework/pull/36600)) +- Use different config key for overriding temporary url host in AwsTemporaryUrl method ([#36612](https://github.com/laravel/framework/pull/36612)) + + +## [v8.32.1 (2021-03-09)](https://github.com/laravel/framework/compare/v8.32.0...v8.32.1) + +### Changed +- Changed `Illuminate\Queue\Middleware\ThrottlesExceptions` ([b8a70e9](https://github.com/laravel/framework/commit/b8a70e9a3685871ed46a24fc03c0267849d2d7c8)) + + +## [v8.32.0 (2021-03-09)](https://github.com/laravel/framework/compare/v8.31.0...v8.32.0) + +Added +- Phpredis lock serialization and compression support ([#36412](https://github.com/laravel/framework/pull/36412), [10f1a93](https://github.com/laravel/framework/commit/10f1a935205340ba8954e7075c1d9b67943db27d)) +- Added Fluent JSON Assertions ([#36454](https://github.com/laravel/framework/pull/36454)) +- Added methods to dump requests of the Laravel HTTP client ([#36466](https://github.com/laravel/framework/pull/36466)) +- Added `ThrottlesExceptions` and `ThrottlesExceptionsWithRedis` job middlewares for unstable services ([#36473](https://github.com/laravel/framework/pull/36473), [21fee76](https://github.com/laravel/framework/commit/21fee7649e1b48a7701b8ba860218741c2c3bcef), [36518](https://github.com/laravel/framework/pull/36518), [37e48ba](https://github.com/laravel/framework/commit/37e48ba864e2f463517429d41cefd94e88136c1c)) +- Added support to Eloquent Collection on `Model::destroy()` ([#36497](https://github.com/laravel/framework/pull/36497)) +- Added `rest` option to `php artisan queue:work` command ([#36521](https://github.com/laravel/framework/pull/36521), [c6ea49c](https://github.com/laravel/framework/commit/c6ea49c80a2ac93aebb8fdf2360161b73cec26af)) +- Added `prohibited_if` and `prohibited_unless` validation rules ([#36516](https://github.com/laravel/framework/pull/36516)) +- Added class `argument` to `Illuminate\Database\Console\Seeds\SeedCommand` ([#36513](https://github.com/laravel/framework/pull/36513)) + +### Fixed +- Fix validator treating null as true for (required|exclude)_(if|unless) due to loose `in_array()` check ([#36504](https://github.com/laravel/framework/pull/36504)) + +### Changed +- Delete existing links that are broken in `Illuminate\Foundation\Console\StorageLinkCommand` ([#36470](https://github.com/laravel/framework/pull/36470)) +- Use user provided url in AwsTemporaryUrl method ([#36480](https://github.com/laravel/framework/pull/36480)) +- Allow to override discover events base path ([#36515](https://github.com/laravel/framework/pull/36515)) + + +## [v8.31.0 (2021-03-04)](https://github.com/laravel/framework/compare/v8.30.1...v8.31.0) + +### Added +- Added new `VendorTagPublished` event ([#36458](https://github.com/laravel/framework/pull/36458)) +- Added new `Stringable::test()` method ([#36462](https://github.com/laravel/framework/pull/36462)) + +### Reverted +- Reverted [Fixed `formatWheres()` methods in `DatabaseRule`](https://github.com/laravel/framework/pull/36441) ([#36452](https://github.com/laravel/framework/pull/36452)) + +### Changed +- Make user policy command fix (Windows) ([#36464](https://github.com/laravel/framework/pull/36464)) + + +## [v8.30.1 (2021-03-03)](https://github.com/laravel/framework/compare/v8.30.0...v8.30.1) + +### Reverted +- Reverted [Respect custom route key with explicit route model binding](https://github.com/laravel/framework/pull/36375) ([#36449](https://github.com/laravel/framework/pull/36449)) + +### Fixed +- Fixed `formatWheres()` methods in `DatabaseRule` ([#36441](https://github.com/laravel/framework/pull/36441)) + + +## [v8.30.0 (2021-03-02)](https://github.com/laravel/framework/compare/v8.29.0...v8.30.0) + +### Added +- Added new line to `DetectsLostConnections` ([#36373](https://github.com/laravel/framework/pull/36373)) +- Added `Illuminate\Cache\RateLimiting\Limit::perMinutes()` ([#36352](https://github.com/laravel/framework/pull/36352), [86d0a5c](https://github.com/laravel/framework/commit/86d0a5c733b3f22ae2353df538e07605963c3052)) +- Make Database Factory macroable ([#36380](https://github.com/laravel/framework/pull/36380)) +- Added stop on first failure for Validators ([39e1f84](https://github.com/laravel/framework/commit/39e1f84a48fec024859d4e80948aca9bd7878658)) +- Added `containsOneItem()` method to Collections ([#36428](https://github.com/laravel/framework/pull/36428), [5b7ffc2](https://github.com/laravel/framework/commit/5b7ffc2b54dec803bd12541ab9c3d6bf3d4666ca)) + +### Changed +- Respect custom route key with explicit route model binding ([#36375](https://github.com/laravel/framework/pull/36375)) +- Add Buffered Console Output ([#36404](https://github.com/laravel/framework/pull/36404)) +- Don't flash 'current_password' input ([#36415](https://github.com/laravel/framework/pull/36415)) +- Check for context method in Exception Handler ([#36424](https://github.com/laravel/framework/pull/36424)) + + +## [v8.29.0 (2021-02-23)](https://github.com/laravel/framework/compare/v8.28.1...v8.29.0) + +### Added +- Support username parameter for predis ([#36299](https://github.com/laravel/framework/pull/36299)) +- Adds "setUpTestDatabase" support to Parallel Testing ([#36301](https://github.com/laravel/framework/pull/36301)) +- Added support closures in sequences ([3c66f6c](https://github.com/laravel/framework/commit/3c66f6cda2ac4ee2844a67fc98e676cb170ff4b1)) +- Added gate evaluation event ([0c6f5f7](https://github.com/laravel/framework/commit/0c6f5f75bf0ba4d3307145c9d92ae022f60414be)) +- Added a `collect` method to the HTTP Client response ([#36331](https://github.com/laravel/framework/pull/36331)) +- Allow Blade's service injection to inject services typed using class name resolution ([#36356](https://github.com/laravel/framework/pull/36356)) + +### Fixed +- Fixed: Using withoutMiddleware() and a closure-based middleware on PHP8 throws an exception ([#36293](https://github.com/laravel/framework/pull/36293)) +- Fixed: The label for page number in pagination links should always be a string ([#36292](https://github.com/laravel/framework/pull/36292)) +- Clean up custom Queue payload between tests ([#36295](https://github.com/laravel/framework/pull/36295)) +- Fixed flushDb (cache:clear) for redis clusters ([#36281](https://github.com/laravel/framework/pull/36281)) +- Fixed retry command for encrypted jobs ([#36334](https://github.com/laravel/framework/pull/36334), [2fb5e44](https://github.com/laravel/framework/commit/2fb5e444ef55a764ba2363a10320e75f3c830504)) +- Make sure `trait_uses_recursive` returns an array ([#36335](https://github.com/laravel/framework/pull/36335)) + +### Changed +- Make use of specified ownerKey in MorphTo::associate() ([#36303](https://github.com/laravel/framework/pull/36303)) +- Update pusher deps and update broadcasting ([3404185](https://github.com/laravel/framework/commit/3404185fbe36139dfbe6d0d9595811b41ee53068)) + + +## [v8.28.1 (2021-02-16)](https://github.com/laravel/framework/compare/v8.28.0...v8.28.1) + +### Fixed +- Revert "[8.x] Clean up custom Queue payload between tests" ([#36287](https://github.com/laravel/framework/pull/36287)) + + +## [v8.28.0 (2021-02-16)](https://github.com/laravel/framework/compare/v8.27.0...v8.28.0) + +### Added +- Allow users to specify configuration keys to be used for primitive binding ([#36241](https://github.com/laravel/framework/pull/36241)) +- ArrayObject + Collection Custom Casts ([#36245](https://github.com/laravel/framework/pull/36245)) +- Add view path method ([af3a651](https://github.com/laravel/framework/commit/af3a651ad6ae3e90bd673fe7a6bfc1ce9e569d25)) + +### Changed +- Allow using dot syntax for `$responseKey` ([#36196](https://github.com/laravel/framework/pull/36196)) +- Full trace for http errors ([#36219](https://github.com/laravel/framework/pull/36219)) + +### Fixed +- Fix undefined property with sole query ([#36216](https://github.com/laravel/framework/pull/36216)) +- Resolving non-instantiables corrupts `Container::$with` ([#36212](https://github.com/laravel/framework/pull/36212)) +- Fix attribute nesting on anonymous components ([#36240](https://github.com/laravel/framework/pull/36240)) +- Ensure `$prefix` is a string ([#36254](https://github.com/laravel/framework/pull/36254)) +- Add missing import ([#34569](https://github.com/laravel/framework/pull/34569)) +- Align PHP 8.1 behavior of `e()` ([#36262](https://github.com/laravel/framework/pull/36262)) +- Ensure null values won't break on PHP 8.1 ([#36264](https://github.com/laravel/framework/pull/36264)) +- Handle directive `$value` as a string ([#36260](https://github.com/laravel/framework/pull/36260)) +- Use explicit flag as default sorting ([#36261](https://github.com/laravel/framework/pull/36261)) +- Fix middleware group display ([d9e28dc](https://github.com/laravel/framework/commit/d9e28dcb1f4a5638b33829d919bd7417321ab39e)) + + +## [v8.27.0 (2021-02-09)](https://github.com/laravel/framework/compare/v8.26.1...v8.27.0) + +### Added +- Conditionally merge classes into a Blade Component attribute bag ([#36131](https://github.com/laravel/framework/pull/36131)) +- Allow adding multiple columns after a column ([#36145](https://github.com/laravel/framework/pull/36145)) +- Add query builder `chunkMap` method ([#36193](https://github.com/laravel/framework/pull/36193), [048ac6d](https://github.com/laravel/framework/commit/048ac6d49f2f7b2d64eb1695848df4590c38be98)) + +### Changed +- Update CallQueuedClosure to catch Throwable/Error ([#36159](https://github.com/laravel/framework/pull/36159)) +- Allow components to use custom attribute bag ([#36186](https://github.com/laravel/framework/pull/36186)) + +### Fixed +- Set process timeout to null for load mysql schema into database ([#36126](https://github.com/laravel/framework/pull/36126)) +- Don't pluralise string if string ends with none alphanumeric character ([#36137](https://github.com/laravel/framework/pull/36137)) +- Add query log methods to the DB facade ([#36177](https://github.com/laravel/framework/pull/36177)) +- Add commonmark as recommended package for `Illuminate\Support` ([#36171](https://github.com/laravel/framework/pull/36171)) +- Fix Eager loading partially nullable morphTo relations ([#36129](https://github.com/laravel/framework/pull/36129)) +- Make height of image working with yahoo ([#36201](https://github.com/laravel/framework/pull/36201)) +- Make `sole()` relationship friendly ([#36200](https://github.com/laravel/framework/pull/36200)) +- Make layout in mail responsive in Gmail app ([#36198](https://github.com/laravel/framework/pull/36198)) +- Fixes parallel testing when a database is configured using URLs ([#36204](https://github.com/laravel/framework/pull/36204)) + + +## [v8.26.1 (2021-02-02)](https://github.com/laravel/framework/compare/v8.26.0...v8.26.1) + +### Fixed +- Fixed merge conflict in `src/Illuminate/Foundation/Console/stubs/exception-render-report.stub` ([#36123](https://github.com/laravel/framework/pull/36123)) + + +## [v8.26.0 (2021-02-02)](https://github.com/laravel/framework/compare/v8.25.0...v8.26.0) + +### Added +- Allow to fillJsonAttribute with encrypted field ([#36063](https://github.com/laravel/framework/pull/36063)) +- Added `Route::missing()` ([#36035](https://github.com/laravel/framework/pull/36035)) +- Added `Illuminate\Support\Str::markdown()` and `Illuminate\Support\Stringable::markdown()` ([#36071](https://github.com/laravel/framework/pull/36071)) +- Support retrieving URL for Sftp adapter ([#36120](https://github.com/laravel/framework/pull/36120)) + +### Fixed +- Fixed issues with dumping PostgreSQL databases that contain multiple schemata ([#36046](https://github.com/laravel/framework/pull/36046)) +- Fixes job batch serialization for PostgreSQL ([#36081](https://github.com/laravel/framework/pull/36081)) +- Fixed `Illuminate\View\ViewException::report()` ([#36110](https://github.com/laravel/framework/pull/36110)) + +### Changed +- Typecast page number as integer in `Illuminate\Pagination\AbstractPaginator::resolveCurrentPage()` ([#36055](https://github.com/laravel/framework/pull/36055)) +- Changed `Illuminate\Testing\ParallelRunner::createApplication()` ([1c11b78](https://github.com/laravel/framework/commit/1c11b7893fa3e9c592f6e85b2b1b0028ddd55645)) + + +## [v8.25.0 (2021-01-26)](https://github.com/laravel/framework/compare/v8.24.0...v8.25.0) + +### Added +- Added `Stringable::pipe` & make Stringable tappable ([#36017](https://github.com/laravel/framework/pull/36017)) +- Accept a command in object form in Bus::assertChained ([#36031](https://github.com/laravel/framework/pull/36031)) +- Adds parallel testing ([#36034](https://github.com/laravel/framework/pull/36034)) +- Make Listeners, Mailables, and Notifications accept ShouldBeEncrypted ([#36036](https://github.com/laravel/framework/pull/36036)) +- Support JSON encoding Stringable ([#36012](https://github.com/laravel/framework/pull/36012)) +- Support for escaping bound attributes ([#36042](https://github.com/laravel/framework/pull/36042)) +- Added `Illuminate\Foundation\Application::useLangPath()` ([#36044](https://github.com/laravel/framework/pull/36044)) + +### Changed +- Pipe through new render and report exception methods ([#36032](https://github.com/laravel/framework/pull/36032)) + +### Fixed +- Fixed issue with dumping schema from a postgres database using no default schema ([#35966](https://github.com/laravel/framework/pull/35966), [7be50a5](https://github.com/laravel/framework/commit/7be50a511955dea2bf4d6e30208b6fbf07eaa36e)) +- Fixed worker --delay option ([#35991](https://github.com/laravel/framework/pull/35991)) +- Added support of PHP 7.3 to RateLimiter middleware(queue) serialization ([#35986](https://github.com/laravel/framework/pull/35986)) +- Fixed `Illuminate\Foundation\Http\Middleware\TransformsRequest::cleanArray()` ([#36002](https://github.com/laravel/framework/pull/36002)) +- ModelNotFoundException: ensure that the model class name is properly set ([#36011](https://github.com/laravel/framework/pull/36011)) +- Fixed bus fake ([e720279](https://github.com/laravel/framework/commit/e72027960fd4d8ff281938edb4632e13e391b8fd)) + + +## [v8.24.0 (2021-01-21)](https://github.com/laravel/framework/compare/v8.23.1...v8.24.0) + +### Added +- Added `JobQueued` event ([8eaec03](https://github.com/laravel/framework/commit/8eaec037421aa9f3860da9d339986448b4c884eb), [5d572e7](https://github.com/laravel/framework/commit/5d572e7a6d479ef68ee92c9d67e2e9465174fb4c)) + +### Fixed +- Fixed type error in `Illuminate\Http\Concerns\InteractsWithContentTypes::isJson()` ([#35956](https://github.com/laravel/framework/pull/35956)) +- Fixed `Illuminate\Collections\Collection::sortByMany()` ([#35950](https://github.com/laravel/framework/pull/35950)) +- Fixed Limit expected bindings ([#35972](https://github.com/laravel/framework/pull/35972), [006873d](https://github.com/laravel/framework/commit/006873df411d28bfd03fea5e7f91a2afe3918498)) +- Fixed serialization of rate limited with redis middleware ([#35971](https://github.com/laravel/framework/pull/35971)) + + +## [v8.23.1 (2021-01-19)](https://github.com/laravel/framework/compare/v8.23.0...v8.23.1) + +### Fixed +- Fixed empty html mail ([#35941](https://github.com/laravel/framework/pull/35941)) + + +## [v8.23.0 (2021-01-19)](https://github.com/laravel/framework/compare/v8.22.1...v8.23.0) + +### Added +- Added `Illuminate\Database\Concerns\BuildsQueries::sole()` ([#35869](https://github.com/laravel/framework/pull/35869), [29c7dae](https://github.com/laravel/framework/commit/29c7dae9b32af2abffa7489f4758fd67905683c3), [#35908](https://github.com/laravel/framework/pull/35908), [#35902](https://github.com/laravel/framework/pull/35902), [#35912](https://github.com/laravel/framework/pull/35912)) +- Added default parameter to throw_if / throw_unless ([#35890](https://github.com/laravel/framework/pull/35890)) +- Added validation support for TeamSpeak3 URI scheme ([#35933](https://github.com/laravel/framework/pull/35933)) + +### Fixed +- Fixed extra space on blade class components that are inline ([#35874](https://github.com/laravel/framework/pull/35874)) +- Fixed serialization of rate limited middleware ([f3d4dcb](https://github.com/laravel/framework/commit/f3d4dcb21dc66824611fdde95c8075b694825bf5), [#35916](https://github.com/laravel/framework/pull/35916)) + +### Changed +- Allow a specific seeder to be used in tests in `Illuminate\Foundation\Testing\RefreshDatabase::migrateFreshUsing()` ([#35864](https://github.com/laravel/framework/pull/35864)) +- Pass $key to closure in Collection and LazyCollection's reduce method as well ([#35878](https://github.com/laravel/framework/pull/35878)) + + +## [v8.22.1 (2021-01-13)](https://github.com/laravel/framework/compare/v8.22.0...v8.22.1) + +### Fixed +- Limit expected bindings ([#35865](https://github.com/laravel/framework/pull/35865)) + + +## [v8.22.0 (2021-01-12)](https://github.com/laravel/framework/compare/v8.21.0...v8.22.0) + +### Added +- Added new lines to `DetectsLostConnections` ([#35752](https://github.com/laravel/framework/pull/35752), [#35790](https://github.com/laravel/framework/pull/35790)) +- Added `Illuminate\Support\Testing\Fakes\EventFake::assertNothingDispatched()` ([#35835](https://github.com/laravel/framework/pull/35835)) +- Added reduce with keys to collections and lazy collections ([#35839](https://github.com/laravel/framework/pull/35839)) + +### Fixed +- Fixed error from missing null check on PHP 8 in `Illuminate\Validation\Concerns\ValidatesAttributes::validateJson()` ([#35797](https://github.com/laravel/framework/pull/35797)) +- Fix bug with RetryCommand ([4415b94](https://github.com/laravel/framework/commit/4415b94623358bfd1dc2e8f20e4deab0025d2d03), [#35828](https://github.com/laravel/framework/pull/35828)) +- Fixed `Illuminate\Testing\PendingCommand::expectsTable()` ([#35820](https://github.com/laravel/framework/pull/35820)) +- Fixed `morphTo()` attempting to map an empty string morph type to an instance ([#35824](https://github.com/laravel/framework/pull/35824)) + +### Changes +- Update `Illuminate\Http\Resources\CollectsResources::collects()` ([1fa20dd](https://github.com/laravel/framework/commit/1fa20dd356af21af6e38d95e9ff2b1d444344fbe)) +- "null" constraint prevents aliasing SQLite ROWID ([#35792](https://github.com/laravel/framework/pull/35792)) +- Allow strings to be passed to the `report` function ([#35803](https://github.com/laravel/framework/pull/35803)) + + +## [v8.21.0 (2021-01-05)](https://github.com/laravel/framework/compare/v8.20.1...v8.21.0) + +### Added +- Added command to clean batches table ([#35694](https://github.com/laravel/framework/pull/35694), [33f5ac6](https://github.com/laravel/framework/commit/33f5ac695a55d6cdbadcfe1b46e3409e4a66df16)) +- Added item to list of causedByLostConnection errors ([#35744](https://github.com/laravel/framework/pull/35744)) +- Make it possible to set Postmark Message Stream ID ([#35755](https://github.com/laravel/framework/pull/35755)) + +### Fixed +- Fixed `php artisan db` command for the Postgres CLI ([#35725](https://github.com/laravel/framework/pull/35725)) +- Fixed OPTIONS method bug with use same path and diff domain when cache route ([#35714](https://github.com/laravel/framework/pull/35714)) + +### Changed +- Ensure DBAL custom type doesn't exists in `Illuminate\Database\DatabaseServiceProvider::registerDoctrineTypes()` ([#35704](https://github.com/laravel/framework/pull/35704)) +- Added missing `dispatchAfterCommit` to `DatabaseQueue` ([#35715](https://github.com/laravel/framework/pull/35715)) +- Set chain queue when inside a batch ([#35746](https://github.com/laravel/framework/pull/35746)) +- Give a more meaningul message when route parameters are missing ([#35706](https://github.com/laravel/framework/pull/35706)) +- Added table prefix to `Illuminate\Database\Console\DumpCommand::schemaState()` ([4ffe40f](https://github.com/laravel/framework/commit/4ffe40fb169c6bcce9193ff56958eca41e64294f)) +- Refresh the retryUntil time on job retry ([#35780](https://github.com/laravel/framework/pull/35780), [45eb7a7](https://github.com/laravel/framework/commit/45eb7a7b1706ae175268731a673f369c0e556805)) + + +## [v8.20.1 (2020-12-22)](https://github.com/laravel/framework/compare/v8.20.0...v8.20.1) + +### Revert +- Revert [Clear a cached user in RequestGuard if a request is changed](https://github.com/laravel/framework/pull/35692) ([ca8ccd6](https://github.com/laravel/framework/commit/ca8ccd6757d5639f0e5fb241b3df6878da6ce34e)) + + +## [v8.20.0 (2020-12-22)](https://github.com/laravel/framework/compare/v8.19.0...v8.20.0) + +### Added +- Added `Illuminate\Database\DBAL\TimestampType` ([a5761d4](https://github.com/laravel/framework/commit/a5761d4187abea654cb422c2f70054a880ffd2e0), [cff3705](https://github.com/laravel/framework/commit/cff37055cbf031109ae769e8fd6ad1951be47aa6) [382445f](https://github.com/laravel/framework/commit/382445f8487de45a05ebe121837f917b92560a97), [810047e](https://github.com/laravel/framework/commit/810047e1f184f8a4def372885591e4fbb6996b51)) +- Added ability to specify a separate lock connection ([#35621](https://github.com/laravel/framework/pull/35621), [3d95235](https://github.com/laravel/framework/commit/3d95235a6ad8525886071ad68e818a225786064f)) +- Added `Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable::syncWithPivotValues()` ([#35644](https://github.com/laravel/framework/pull/35644), [49b3ce0](https://github.com/laravel/framework/commit/49b3ce098d8a612797b195c4e3774b1e00c604c8)) + +### Fixed +- Fixed `Illuminate\Validation\Concerns\ValidatesAttributes::validateJson()` for PHP8 ([#35646](https://github.com/laravel/framework/pull/35646)) +- Fixed `assertCookieExpired()` and `assertCookieNotExpired()` methods in `Illuminate\Testing\TestResponse` ([#35637](https://github.com/laravel/framework/pull/35637)) +- Fixed: Account for a numerical array of views in Mailable::renderForAssertions() ([#35662](https://github.com/laravel/framework/pull/35662)) +- Catch DecryptException with invalid X-XSRF-TOKEN in `Illuminate\Foundation\Http\Middleware\VerifyCsrfToken` ([#35671](https://github.com/laravel/framework/pull/35671)) + +### Changed +- Check configuration in `Illuminate\Foundation\Console\Kernel::scheduleCache()` ([a253d0e](https://github.com/laravel/framework/commit/a253d0e40d3deb293d54df9f4455879af5365aab)) +- Modify `Model::mergeCasts` to return `$this` ([#35683](https://github.com/laravel/framework/pull/35683)) +- Clear a cached user in RequestGuard if a request is changed ([#35692](https://github.com/laravel/framework/pull/35692)) ## [v8.19.0 (2020-12-15)](https://github.com/laravel/framework/compare/v8.18.1...v8.19.0) diff --git a/composer.json b/composer.json index 581d0ead203b..401a6e952839 100644 --- a/composer.json +++ b/composer.json @@ -129,6 +129,7 @@ "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).", + "brianium/paratest": "Required to run tests in parallel (^6.0).", "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).", "filp/whoops": "Required for friendly error pages in development (^2.8).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", @@ -143,7 +144,7 @@ "phpunit/phpunit": "Required to use assertions and run tests (^8.5.8|^9.3.3).", "predis/predis": "Required to use the predis connector (^1.1.2).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0|^5.0).", "symfony/cache": "Required to PSR-6 cache bridge (^5.1.4).", "symfony/filesystem": "Required to enable support for relative symbolic links (^5.1.4).", "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0).", diff --git a/src/Illuminate/Auth/Access/Events/GateEvaluated.php b/src/Illuminate/Auth/Access/Events/GateEvaluated.php new file mode 100644 index 000000000000..f77a9c84c51b --- /dev/null +++ b/src/Illuminate/Auth/Access/Events/GateEvaluated.php @@ -0,0 +1,51 @@ +user = $user; + $this->ability = $ability; + $this->result = $result; + $this->arguments = $arguments; + } +} diff --git a/src/Illuminate/Auth/Access/Gate.php b/src/Illuminate/Auth/Access/Gate.php index 7fc5d90717a8..0bcc0e35844d 100644 --- a/src/Illuminate/Auth/Access/Gate.php +++ b/src/Illuminate/Auth/Access/Gate.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Contracts\Auth\Access\Gate as GateContract; use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -374,9 +375,11 @@ public function raw($ability, $arguments = []) // After calling the authorization callback, we will call the "after" callbacks // that are registered with the Gate, which allows a developer to do logging // if that is required for this application. Then we'll return the result. - return $this->callAfterCallbacks( + return tap($this->callAfterCallbacks( $user, $ability, $arguments, $result - ); + ), function ($result) use ($user, $ability, $arguments) { + $this->dispatchGateEvaluatedEvent($user, $ability, $arguments, $result); + }); } /** @@ -519,6 +522,24 @@ protected function callAfterCallbacks($user, $ability, array $arguments, $result return $result; } + /** + * Dispatch a gate evaluation event. + * + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @param string $ability + * @param array $arguments + * @param bool|null $result + * @return void + */ + protected function dispatchGateEvaluatedEvent($user, $ability, array $arguments, $result) + { + if ($this->container->bound(Dispatcher::class)) { + $this->container->make(Dispatcher::class)->dispatch( + new Events\GateEvaluated($user, $ability, $result, $arguments) + ); + } + } + /** * Resolve the callable for the given ability and arguments. * @@ -779,4 +800,17 @@ public function policies() { return $this->policies; } + + /** + * Set the container instance used by the gate. + * + * @param \Illuminate\Contracts\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + + return $this; + } } diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index ebbd7f5f1ac5..823b96ca319f 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -295,6 +295,31 @@ public function hasResolvedGuards() return count($this->guards) > 0; } + /** + * Forget all of the resolved guard instances. + * + * @return $this + */ + public function forgetGuards() + { + $this->guards = []; + + return $this; + } + + /** + * Set the application instance used by the manager. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + return $this; + } + /** * Dynamically call the default driver instance. * diff --git a/src/Illuminate/Auth/AuthServiceProvider.php b/src/Illuminate/Auth/AuthServiceProvider.php index 7a6b41212784..9c17edfa1c6f 100755 --- a/src/Illuminate/Auth/AuthServiceProvider.php +++ b/src/Illuminate/Auth/AuthServiceProvider.php @@ -35,11 +35,6 @@ public function register() protected function registerAuthenticator() { $this->app->singleton('auth', function ($app) { - // Once the authentication service has actually been requested by the developer - // we will set a variable in the application indicating such. This helps us - // know that we need to set any queued cookies in the after event later. - $app['auth.loaded'] = true; - return new AuthManager($app); }); @@ -55,11 +50,9 @@ protected function registerAuthenticator() */ protected function registerUserResolver() { - $this->app->bind( - AuthenticatableContract::class, function ($app) { - return call_user_func($app['auth']->userResolver()); - } - ); + $this->app->bind(AuthenticatableContract::class, function ($app) { + return call_user_func($app['auth']->userResolver()); + }); } /** @@ -83,15 +76,13 @@ protected function registerAccessGate() */ protected function registerRequirePassword() { - $this->app->bind( - RequirePassword::class, function ($app) { - return new RequirePassword( - $app[ResponseFactory::class], - $app[UrlGenerator::class], - $app['config']->get('auth.password_timeout') - ); - } - ); + $this->app->bind(RequirePassword::class, function ($app) { + return new RequirePassword( + $app[ResponseFactory::class], + $app[UrlGenerator::class], + $app['config']->get('auth.password_timeout') + ); + }); } /** @@ -116,11 +107,8 @@ protected function registerRequestRebindHandler() protected function registerEventRebindHandler() { $this->app->rebinding('events', function ($app, $dispatcher) { - if (! $app->resolved('auth')) { - return; - } - - if ($app['auth']->hasResolvedGuards() === false) { + if (! $app->resolved('auth') || + $app['auth']->hasResolvedGuards() === false) { return; } diff --git a/src/Illuminate/Auth/GuardHelpers.php b/src/Illuminate/Auth/GuardHelpers.php index 4d5328c6bc87..aa9ebf9ec64a 100644 --- a/src/Illuminate/Auth/GuardHelpers.php +++ b/src/Illuminate/Auth/GuardHelpers.php @@ -25,7 +25,7 @@ trait GuardHelpers protected $provider; /** - * Determine if current user is authenticated. If not, throw an exception. + * Determine if the current user is authenticated. If not, throw an exception. * * @return \Illuminate\Contracts\Auth\Authenticatable * diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 1e6f8c2b5165..4bb3fd4b6f73 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use RuntimeException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; @@ -320,7 +321,7 @@ protected function attemptBasic(Request $request, $field, $extraConditions = []) } /** - * Get the credential array for a HTTP Basic request. + * Get the credential array for an HTTP Basic request. * * @param \Symfony\Component\HttpFoundation\Request $request * @param string $field @@ -581,6 +582,8 @@ protected function cycleRememberToken(AuthenticatableContract $user) * @param string $password * @param string $attribute * @return bool|null + * + * @throws \Illuminate\Auth\AuthenticationException */ public function logoutOtherDevices($password, $attribute = 'password') { @@ -588,9 +591,7 @@ public function logoutOtherDevices($password, $attribute = 'password') return; } - $result = tap($this->user()->forceFill([ - $attribute => Hash::make($password), - ]))->save(); + $result = $this->rehashUserPassword($password, $attribute); if ($this->recaller() || $this->getCookieJar()->hasQueued($this->getRecallerName())) { @@ -602,6 +603,26 @@ public function logoutOtherDevices($password, $attribute = 'password') return $result; } + /** + * Rehash the current user's password. + * + * @param string $password + * @param string $attribute + * @return bool|null + * + * @throws \InvalidArgumentException + */ + protected function rehashUserPassword($password, $attribute) + { + if (! Hash::check($password, $this->user()->{$attribute})) { + throw new InvalidArgumentException('The given password does not match the current password.'); + } + + return tap($this->user()->forceFill([ + $attribute => Hash::make($password), + ]))->save(); + } + /** * Register an authentication attempt event listener. * diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index e5ec7346e813..833a19948b8a 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -334,6 +334,41 @@ public function extend($driver, Closure $callback) return $this; } + /** + * Get the application instance used by the manager. + * + * @return \Illuminate\Contracts\Foundation\Application + */ + public function getApplication() + { + return $this->app; + } + + /** + * Set the application instance used by the manager. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + return $this; + } + + /** + * Forget all of the resolved driver instances. + * + * @return $this + */ + public function forgetDrivers() + { + $this->drivers = []; + + return $this; + } + /** * Dynamically call the default driver instance. * diff --git a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php index 7857c26dce3f..63927dd0181d 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php @@ -120,7 +120,7 @@ public function broadcast(array $channels, $event, array $payload = []) } /** - * Return true if channel is protected by authentication. + * Return true if the channel is protected by authentication. * * @param string $channel * @return bool diff --git a/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php index f55cf02b86d4..15695e114e6f 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php @@ -5,6 +5,7 @@ use Illuminate\Broadcasting\BroadcastException; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Pusher\ApiErrorException; use Pusher\Pusher; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -110,20 +111,44 @@ public function broadcast(array $channels, $event, array $payload = []) { $socket = Arr::pull($payload, 'socket'); - $response = $this->pusher->trigger( - $this->formatChannels($channels), $event, $payload, $socket, true - ); + if ($this->pusherServerIsVersionFiveOrGreater()) { + $parameters = $socket !== null ? ['socket_id' => $socket] : []; + + try { + $this->pusher->trigger( + $this->formatChannels($channels), $event, $payload, $parameters + ); + } catch (ApiErrorException $e) { + throw new BroadcastException( + sprintf('Pusher error: %s.', $e->getMessage()) + ); + } + } else { + $response = $this->pusher->trigger( + $this->formatChannels($channels), $event, $payload, $socket, true + ); + + if ((is_array($response) && $response['status'] >= 200 && $response['status'] <= 299) + || $response === true) { + return; + } - if ((is_array($response) && $response['status'] >= 200 && $response['status'] <= 299) - || $response === true) { - return; + throw new BroadcastException( + ! empty($response['body']) + ? sprintf('Pusher error: %s.', $response['body']) + : 'Failed to connect to Pusher.' + ); } + } - throw new BroadcastException( - ! empty($response['body']) - ? sprintf('Pusher error: %s.', $response['body']) - : 'Failed to connect to Pusher.' - ); + /** + * Determine if the Pusher PHP server is version 5.0 or greater. + * + * @return bool + */ + protected function pusherServerIsVersionFiveOrGreater() + { + return class_exists(ApiErrorException::class); } /** diff --git a/src/Illuminate/Broadcasting/Broadcasters/UsePusherChannelConventions.php b/src/Illuminate/Broadcasting/Broadcasters/UsePusherChannelConventions.php index 07c707ceb046..690cf3d4aca2 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/UsePusherChannelConventions.php +++ b/src/Illuminate/Broadcasting/Broadcasters/UsePusherChannelConventions.php @@ -7,7 +7,7 @@ trait UsePusherChannelConventions { /** - * Return true if channel is protected by authentication. + * Return true if the channel is protected by authentication. * * @param string $channel * @return bool diff --git a/src/Illuminate/Broadcasting/composer.json b/src/Illuminate/Broadcasting/composer.json index 3a7fe618a450..45c30271c084 100644 --- a/src/Illuminate/Broadcasting/composer.json +++ b/src/Illuminate/Broadcasting/composer.json @@ -34,7 +34,7 @@ } }, "suggest": { - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0)." + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0|^5.0)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Bus/Batch.php b/src/Illuminate/Bus/Batch.php index 1073ecd46d37..cac16e1e9f51 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -170,7 +170,10 @@ public function add($jobs) $count += count($job); return with($this->prepareBatchedChain($job), function ($chain) { - return $chain->first()->chain($chain->slice(1)->values()->all()); + return $chain->first() + ->allOnQueue($this->options['queue'] ?? null) + ->allOnConnection($this->options['connection'] ?? null) + ->chain($chain->slice(1)->values()->all()); }); } else { $job->withBatchId($this->id); diff --git a/src/Illuminate/Bus/DatabaseBatchRepository.php b/src/Illuminate/Bus/DatabaseBatchRepository.php index d911c380d551..03d7a3dab3cb 100644 --- a/src/Illuminate/Bus/DatabaseBatchRepository.php +++ b/src/Illuminate/Bus/DatabaseBatchRepository.php @@ -4,11 +4,13 @@ use Carbon\CarbonImmutable; use Closure; +use DateTimeInterface; use Illuminate\Database\Connection; +use Illuminate\Database\PostgresConnection; use Illuminate\Database\Query\Expression; use Illuminate\Support\Str; -class DatabaseBatchRepository implements BatchRepository +class DatabaseBatchRepository implements PrunableBatchRepository { /** * The batch factory instance. @@ -101,7 +103,7 @@ public function store(PendingBatch $batch) 'pending_jobs' => 0, 'failed_jobs' => 0, 'failed_job_ids' => '[]', - 'options' => serialize($batch->options), + 'options' => $this->serialize($batch->options), 'created_at' => time(), 'cancelled_at' => null, 'finished_at' => null, @@ -230,6 +232,29 @@ public function delete(string $batchId) $this->connection->table($this->table)->where('id', $batchId)->delete(); } + /** + * Prune all of the entries older than the given date. + * + * @param \DateTimeInterface $before + * @return int + */ + public function prune(DateTimeInterface $before) + { + $query = $this->connection->table($this->table) + ->whereNotNull('finished_at') + ->where('finished_at', '<', $before->getTimestamp()); + + $totalDeleted = 0; + + do { + $deleted = $query->take(1000)->delete(); + + $totalDeleted += $deleted; + } while ($deleted !== 0); + + return $totalDeleted; + } + /** * Execute the given Closure within a storage specific transaction. * @@ -243,6 +268,37 @@ public function transaction(Closure $callback) }); } + /** + * Serialize the given value. + * + * @param mixed $value + * @return string + */ + protected function serialize($value) + { + $serialized = serialize($value); + + return $this->connection instanceof PostgresConnection + ? base64_encode($serialized) + : $serialized; + } + + /** + * Unserialize the given value. + * + * @param string $serialized + * @return mixed + */ + protected function unserialize($serialized) + { + if ($this->connection instanceof PostgresConnection && + ! Str::contains($serialized, [':', ';'])) { + $serialized = base64_decode($serialized); + } + + return unserialize($serialized); + } + /** * Convert the given raw batch to a Batch object. * @@ -259,7 +315,7 @@ protected function toBatch($batch) (int) $batch->pending_jobs, (int) $batch->failed_jobs, json_decode($batch->failed_job_ids, true), - unserialize($batch->options), + $this->unserialize($batch->options), CarbonImmutable::createFromTimestamp($batch->created_at), $batch->cancelled_at ? CarbonImmutable::createFromTimestamp($batch->cancelled_at) : $batch->cancelled_at, $batch->finished_at ? CarbonImmutable::createFromTimestamp($batch->finished_at) : $batch->finished_at diff --git a/src/Illuminate/Bus/PrunableBatchRepository.php b/src/Illuminate/Bus/PrunableBatchRepository.php new file mode 100644 index 000000000000..3f972553b597 --- /dev/null +++ b/src/Illuminate/Bus/PrunableBatchRepository.php @@ -0,0 +1,16 @@ + $config['region'], - 'version' => 'latest', - 'endpoint' => $config['endpoint'] ?? null, - ]; - - if ($config['key'] && $config['secret']) { - $dynamoConfig['credentials'] = Arr::only( - $config, ['key', 'secret', 'token'] - ); - } - return $this->repository( new DynamoDbStore( - new DynamoDbClient($dynamoConfig), + $this->app['cache.dynamodb.client'], $config['table'], $config['attributes']['key'] ?? 'key', $config['attributes']['value'] ?? 'value', diff --git a/src/Illuminate/Cache/CacheServiceProvider.php b/src/Illuminate/Cache/CacheServiceProvider.php index e0616768e373..4e2db4f210d2 100755 --- a/src/Illuminate/Cache/CacheServiceProvider.php +++ b/src/Illuminate/Cache/CacheServiceProvider.php @@ -2,7 +2,9 @@ namespace Illuminate\Cache; +use Aws\DynamoDb\DynamoDbClient; use Illuminate\Contracts\Support\DeferrableProvider; +use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; use Symfony\Component\Cache\Adapter\Psr16Adapter; @@ -31,6 +33,24 @@ public function register() return new MemcachedConnector; }); + $this->app->singleton('cache.dynamodb.client', function ($app) { + $config = $app['config']->get('cache.stores.dynamodb'); + + $dynamoConfig = [ + 'region' => $config['region'], + 'version' => 'latest', + 'endpoint' => $config['endpoint'] ?? null, + ]; + + if ($config['key'] && $config['secret']) { + $dynamoConfig['credentials'] = Arr::only( + $config, ['key', 'secret', 'token'] + ); + } + + return new DynamoDbClient($dynamoConfig); + }); + $this->app->singleton(RateLimiter::class); } @@ -42,7 +62,7 @@ public function register() public function provides() { return [ - 'cache', 'cache.store', 'cache.psr6', 'memcached.connector', RateLimiter::class, + 'cache', 'cache.store', 'cache.psr6', 'memcached.connector', 'cache.dynamodb.client', RateLimiter::class, ]; } } diff --git a/src/Illuminate/Cache/Lock.php b/src/Illuminate/Cache/Lock.php index 271cba50fc58..bed170507a9a 100644 --- a/src/Illuminate/Cache/Lock.php +++ b/src/Illuminate/Cache/Lock.php @@ -105,7 +105,7 @@ public function get($callback = null) * * @param int $seconds * @param callable|null $callback - * @return bool + * @return mixed * * @throws \Illuminate\Contracts\Cache\LockTimeoutException */ @@ -153,7 +153,7 @@ protected function isOwnedByCurrentProcess() } /** - * Specify the number of milliseconds to sleep in between blocked lock aquisition attempts. + * Specify the number of milliseconds to sleep in between blocked lock acquisition attempts. * * @param int $milliseconds * @return $this diff --git a/src/Illuminate/Cache/PhpRedisLock.php b/src/Illuminate/Cache/PhpRedisLock.php new file mode 100644 index 000000000000..9d0215f37d6d --- /dev/null +++ b/src/Illuminate/Cache/PhpRedisLock.php @@ -0,0 +1,110 @@ +redis->eval( + LuaScripts::releaseLock(), + 1, + $this->name, + $this->serializedAndCompressedOwner() + ); + } + + /** + * Get the owner key, serialized and compressed. + * + * @return string + */ + protected function serializedAndCompressedOwner(): string + { + $client = $this->redis->client(); + + $owner = $client->_serialize($this->owner); + + // https://github.com/phpredis/phpredis/issues/1938 + if ($this->compressed()) { + if ($this->lzfCompressed()) { + $owner = \lzf_compress($owner); + } elseif ($this->zstdCompressed()) { + $owner = \zstd_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); + } elseif ($this->lz4Compressed()) { + $owner = \lz4_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); + } else { + throw new UnexpectedValueException(sprintf( + 'Unknown phpredis compression in use [%d]. Unable to release lock.', + $client->getOption(Redis::OPT_COMPRESSION) + )); + } + } + + return $owner; + } + + /** + * Determine if compression is enabled. + * + * @return bool + */ + protected function compressed(): bool + { + return $this->redis->client()->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; + } + + /** + * Determine if LZF compression is enabled. + * + * @return bool + */ + protected function lzfCompressed(): bool + { + return defined('Redis::COMPRESSION_LZF') && + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; + } + + /** + * Determine if ZSTD compression is enabled. + * + * @return bool + */ + protected function zstdCompressed(): bool + { + return defined('Redis::COMPRESSION_ZSTD') && + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; + } + + /** + * Determine if LZ4 compression is enabled. + * + * @return bool + */ + protected function lz4Compressed(): bool + { + return defined('Redis::COMPRESSION_LZ4') && + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; + } +} diff --git a/src/Illuminate/Cache/RateLimiting/Limit.php b/src/Illuminate/Cache/RateLimiting/Limit.php index ab3463f51830..330cab39bba1 100644 --- a/src/Illuminate/Cache/RateLimiting/Limit.php +++ b/src/Illuminate/Cache/RateLimiting/Limit.php @@ -58,6 +58,18 @@ public static function perMinute($maxAttempts) return new static('', $maxAttempts); } + /** + * Create a new rate limit using minutes as decay time. + * + * @param int $decayMinutes + * @param int $maxAttempts + * @return static + */ + public static function perMinutes($decayMinutes, $maxAttempts) + { + return new static('', $maxAttempts, $decayMinutes); + } + /** * Create a new rate limit using hours as decay time. * diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index cdf1c8fca094..4896c9183d03 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Redis\Factory as Redis; +use Illuminate\Redis\Connections\PhpRedisConnection; class RedisStore extends TaggableStore implements LockProvider { @@ -188,7 +189,15 @@ public function forever($key, $value) */ public function lock($name, $seconds = 0, $owner = null) { - return new RedisLock($this->lockConnection(), $this->prefix.$name, $seconds, $owner); + $lockName = $this->prefix.$name; + + $lockConnection = $this->lockConnection(); + + if ($lockConnection instanceof PhpRedisConnection) { + return new PhpRedisLock($lockConnection, $lockName, $seconds, $owner); + } + + return new RedisLock($lockConnection, $lockName, $seconds, $owner); } /** diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 46af170f2b40..00cd39b013a4 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -292,8 +292,12 @@ public function setMultiple($values, $ttl = null) */ public function add($key, $value, $ttl = null) { + $seconds = null; + if ($ttl !== null) { - if ($this->getSeconds($ttl) <= 0) { + $seconds = $this->getSeconds($ttl); + + if ($seconds <= 0) { return false; } @@ -301,8 +305,6 @@ public function add($key, $value, $ttl = null) // has a chance to override this logic. Some drivers better support the way // this operation should work with a total "atomic" implementation of it. if (method_exists($this->store, 'add')) { - $seconds = $this->getSeconds($ttl); - return $this->store->add( $this->itemKey($key), $value, $seconds ); @@ -313,7 +315,7 @@ public function add($key, $value, $ttl = null) // so it exists for subsequent requests. Then, we will return true so it is // easy to know if the value gets added. Otherwise, we will return false. if (is_null($this->get($key))) { - return $this->put($key, $value, $ttl); + return $this->put($key, $value, $seconds); } return false; diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index f4c7b4007a91..68fb3cecc100 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -553,6 +553,16 @@ public function isEmpty() return empty($this->items); } + /** + * Determine if the collection contains a single item. + * + * @return bool + */ + public function containsOneItem() + { + return $this->count() === 1; + } + /** * Join all items from the collection using a string. The final items can use a separate glue string. * @@ -872,18 +882,6 @@ public function random($number = null) return new static(Arr::random($this->items, $number)); } - /** - * Reduce the collection to a single value. - * - * @param callable $callback - * @param mixed $initial - * @return mixed - */ - public function reduce(callable $callback, $initial = null) - { - return array_reduce($this->items, $callback, $initial); - } - /** * Replace the collection items with the given items. * @@ -1098,7 +1096,7 @@ public function sort($callback = null) $callback && is_callable($callback) ? uasort($items, $callback) - : asort($items, $callback); + : asort($items, $callback ?? SORT_REGULAR); return new static($items); } @@ -1180,7 +1178,7 @@ protected function sortByMany(array $comparisons = []) if (is_callable($prop)) { $result = $prop($a, $b); } else { - $values = [Arr::get($a, $prop), Arr::get($b, $prop)]; + $values = [data_get($a, $prop), data_get($b, $prop)]; if (! $ascending) { $values = array_reverse($values); diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 297ee2f08df5..4bda35476905 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -415,9 +415,9 @@ public function whereNotIn($key, $values, $strict = false); public function whereNotInStrict($key, $values); /** - * Filter the items, removing any items that don't match the given type. + * Filter the items, removing any items that don't match the given type(s). * - * @param string $type + * @param string|string[] $type * @return static */ public function whereInstanceOf($type); diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 2384948f951a..8fcf035c9702 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -5,6 +5,7 @@ use ArrayIterator; use Closure; use DateTimeInterface; +use Generator; use Illuminate\Support\Traits\EnumeratesValues; use Illuminate\Support\Traits\Macroable; use IteratorAggregate; @@ -29,7 +30,7 @@ class LazyCollection implements Enumerable */ public function __construct($source = null) { - if ($source instanceof Closure || $source instanceof self) { + if ($source instanceof Closure || $source instanceof Generator || $source instanceof self) { $this->source = $source; } elseif (is_null($source)) { $this->source = static::empty(); @@ -547,7 +548,7 @@ public function intersectByKeys($items) } /** - * Determine if the items is empty or not. + * Determine if the items are empty or not. * * @return bool */ @@ -556,6 +557,16 @@ public function isEmpty() return ! $this->getIterator()->valid(); } + /** + * Determine if the collection contains a single item. + * + * @return bool + */ + public function containsOneItem() + { + return $this->take(2)->count() === 1; + } + /** * Join all items from the collection using a string. The final items can use a separate glue string. * @@ -827,24 +838,6 @@ public function random($number = null) return is_null($number) ? $result : new static($result); } - /** - * Reduce the collection to a single value. - * - * @param callable $callback - * @param mixed $initial - * @return mixed - */ - public function reduce(callable $callback, $initial = null) - { - $result = $initial; - - foreach ($this as $value) { - $result = $callback($result, $value); - } - - return $result; - } - /** * Replace the collection items with the given items. * @@ -1079,7 +1072,7 @@ public function chunkWhile(callable $callback) return new static(function () use ($callback) { $iterator = $this->getIterator(); - $chunk = new Collection(); + $chunk = new Collection; if ($iterator->valid()) { $chunk[$iterator->key()] = $iterator->current(); @@ -1091,7 +1084,7 @@ public function chunkWhile(callable $callback) if (! $callback($iterator->current(), $iterator->key(), $chunk)) { yield new static($chunk); - $chunk = new Collection(); + $chunk = new Collection; } $chunk[$iterator->key()] = $iterator->current(); @@ -1372,6 +1365,10 @@ public function count() */ protected function makeIterator($source) { + if ($source instanceof Generator) { + return $source; + } + if ($source instanceof IteratorAggregate) { return $source->getIterator(); } diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index 3355a9b2da29..865d0047cdd5 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -669,14 +669,24 @@ public function whereNotInStrict($key, $values) } /** - * Filter the items, removing any items that don't match the given type. + * Filter the items, removing any items that don't match the given type(s). * - * @param string $type + * @param string|string[] $type * @return static */ public function whereInstanceOf($type) { return $this->filter(function ($value) use ($type) { + if (is_array($type)) { + foreach ($type as $classType) { + if ($value instanceof $classType) { + return true; + } + } + + return false; + } + return $value instanceof $type; }); } @@ -716,6 +726,42 @@ public function tap(callable $callback) return $this; } + /** + * Reduce the collection to a single value. + * + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + $result = $initial; + + foreach ($this as $key => $value) { + $result = $callback($result, $value, $key); + } + + return $result; + } + + /** + * Reduce an associative collection to a single value. + * + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduceWithKeys(callable $callback, $initial = null) + { + $result = $initial; + + foreach ($this as $key => $value) { + $result = $callback($result, $value, $key); + } + + return $result; + } + /** * Create a collection of all elements that do not pass a given truth test. * diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 6ae6dfe68a9b..67669e5ce1c6 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -179,8 +179,8 @@ function last($array) * @param mixed $value * @return mixed */ - function value($value) + function value($value, ...$args) { - return $value instanceof Closure ? $value() : $value; + return $value instanceof Closure ? $value(...$args) : $value; } } diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 7066c8485425..345ab941116e 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -19,7 +19,6 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\BufferedOutput; -use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\PhpExecutableFinder; @@ -86,7 +85,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null $this->events->dispatch( new CommandStarting( - $commandName, $input, $output = $output ?: new ConsoleOutput + $commandName, $input, $output = $output ?: new BufferedConsoleOutput ) ); diff --git a/src/Illuminate/Console/BufferedConsoleOutput.php b/src/Illuminate/Console/BufferedConsoleOutput.php new file mode 100644 index 000000000000..4bb5ca228541 --- /dev/null +++ b/src/Illuminate/Console/BufferedConsoleOutput.php @@ -0,0 +1,41 @@ +buffer, function () { + $this->buffer = ''; + }); + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $message, bool $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + + return parent::doWrite($message, $newline); + } +} diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 0bfaeaf8c429..b3ab9b2db567 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -87,7 +87,7 @@ class Event public $expiresAt = 1440; /** - * Indicates if the command should run in background. + * Indicates if the command should run in the background. * * @var bool */ @@ -328,7 +328,7 @@ protected function expressionPasses() $date->setTimezone($this->timezone); } - return CronExpression::factory($this->expression)->isDue($date->toDateTimeString()); + return (new CronExpression($this->expression))->isDue($date->toDateTimeString()); } /** @@ -587,7 +587,7 @@ protected function pingCallback($url) } /** - * State that the command should run in background. + * State that the command should run in the background. * * @return $this */ @@ -890,9 +890,8 @@ public function getSummaryForDisplay() */ public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) { - return Date::instance(CronExpression::factory( - $this->getExpression() - )->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone)); + return Date::instance((new CronExpression($this->getExpression())) + ->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone)); } /** diff --git a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php index ec296ebd4972..f30a2f0c9086 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php @@ -51,7 +51,7 @@ public function handle() if (! empty($output)) { if ($key !== $keyOfLastExecutionWithOutput) { - $this->info(PHP_EOL.'Execution #'.($key + 1).' output:'); + $this->info(PHP_EOL.'['.date('c').'] Execution #'.($key + 1).' output:'); $keyOfLastExecutionWithOutput = $key; } diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 765df0d873d8..642d1e082e6d 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -6,6 +6,7 @@ use Closure; use Exception; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Container\CircularDependencyException; use Illuminate\Contracts\Container\Container as ContainerContract; use LogicException; use ReflectionClass; @@ -659,7 +660,7 @@ public function get($id) try { return $this->resolve($id); } catch (Exception $e) { - if ($this->has($id)) { + if ($this->has($id) || $e instanceof CircularDependencyException) { throw $e; } @@ -676,6 +677,7 @@ public function get($id) * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Illuminate\Contracts\Container\CircularDependencyException */ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { @@ -816,6 +818,7 @@ protected function isBuildable($concrete, $abstract) * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Illuminate\Contracts\Container\CircularDependencyException */ public function build($concrete) { @@ -839,6 +842,10 @@ public function build($concrete) return $this->notInstantiable($concrete); } + // if (in_array($concrete, $this->buildStack)) { + // throw new CircularDependencyException("Circular dependency detected while resolving [{$concrete}]."); + // } + $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); @@ -985,10 +992,14 @@ protected function resolveClass(ReflectionParameter $parameter) // the value of the dependency, similarly to how we do this with scalars. catch (BindingResolutionException $e) { if ($parameter->isDefaultValueAvailable()) { + array_pop($this->with); + return $parameter->getDefaultValue(); } if ($parameter->isVariadic()) { + array_pop($this->with); + return []; } diff --git a/src/Illuminate/Container/ContextualBindingBuilder.php b/src/Illuminate/Container/ContextualBindingBuilder.php index 5da6ccab388b..1d15dcd3da6a 100644 --- a/src/Illuminate/Container/ContextualBindingBuilder.php +++ b/src/Illuminate/Container/ContextualBindingBuilder.php @@ -81,4 +81,18 @@ public function giveTagged($tag) return is_array($taggedServices) ? $taggedServices : iterator_to_array($taggedServices); }); } + + /** + * Specify the configuration item to bind as a primitive. + * + * @param string $key + * @param ?string $default + * @return void + */ + public function giveConfig($key, $default = null) + { + $this->give(function ($container) use ($key, $default) { + return $container->get('config')->get($key, $default); + }); + } } diff --git a/src/Illuminate/Contracts/Cache/Lock.php b/src/Illuminate/Contracts/Cache/Lock.php index 7f01b1be3f33..03f633a07a21 100644 --- a/src/Illuminate/Contracts/Cache/Lock.php +++ b/src/Illuminate/Contracts/Cache/Lock.php @@ -17,7 +17,7 @@ public function get($callback = null); * * @param int $seconds * @param callable|null $callback - * @return bool + * @return mixed */ public function block($seconds, $callback = null); diff --git a/src/Illuminate/Contracts/Container/CircularDependencyException.php b/src/Illuminate/Contracts/Container/CircularDependencyException.php new file mode 100644 index 000000000000..6c90381cc0cd --- /dev/null +++ b/src/Illuminate/Contracts/Container/CircularDependencyException.php @@ -0,0 +1,11 @@ +queued); } + + /** + * Flush the cookies which have been queued for the next request. + * + * @return $this + */ + public function flushQueuedCookies() + { + $this->queued = []; + + return $this; + } } diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index 6a39b4cb8fc4..b35cf60c40f5 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -3,8 +3,13 @@ namespace Illuminate\Database\Concerns; use Illuminate\Container\Container; +use Illuminate\Database\MultipleRecordsFoundException; +use Illuminate\Database\RecordsNotFoundException; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; +use Illuminate\Support\Collection; +use Illuminate\Support\LazyCollection; +use InvalidArgumentException; trait BuildsQueries { @@ -48,6 +53,26 @@ public function chunk($count, callable $callback) return true; } + /** + * Run a map over each item while chunking. + * + * @param callable $callback + * @param int $count + * @return \Illuminate\Support\Collection + */ + public function chunkMap(callable $callback, $count = 1000) + { + $collection = Collection::make(); + + $this->chunk($count, function ($items) use ($collection, $callback) { + $items->each(function ($item) use ($collection, $callback) { + $collection->push($callback($item)); + }); + }); + + return $collection; + } + /** * Execute a callback over each item while chunking. * @@ -136,6 +161,76 @@ public function eachById(callable $callback, $count = 1000, $column = null, $ali }, $column, $alias); } + /** + * Query lazily, by chunks of the given size. + * + * @param int $chunkSize + * @return \Illuminate\Support\LazyCollection + */ + public function lazy($chunkSize = 1000) + { + if ($chunkSize < 1) { + throw new InvalidArgumentException('The chunk size should be at least 1'); + } + + $this->enforceOrderBy(); + + return LazyCollection::make(function () use ($chunkSize) { + $page = 1; + + while (true) { + $results = $this->forPage($page++, $chunkSize)->get(); + + foreach ($results as $result) { + yield $result; + } + + if ($results->count() < $chunkSize) { + return; + } + } + }); + } + + /** + * Query lazily, by chunking the results of a query by comparing IDs. + * + * @param int $count + * @param string|null $column + * @param string|null $alias + * @return \Illuminate\Support\LazyCollection + */ + public function lazyById($chunkSize = 1000, $column = null, $alias = null) + { + if ($chunkSize < 1) { + throw new InvalidArgumentException('The chunk size should be at least 1'); + } + + $column = $column ?? $this->defaultKeyName(); + + $alias = $alias ?? $column; + + return LazyCollection::make(function () use ($chunkSize, $column, $alias) { + $lastId = null; + + while (true) { + $clone = clone $this; + + $results = $clone->forPageAfterId($chunkSize, $lastId, $column)->get(); + + foreach ($results as $result) { + yield $result; + } + + if ($results->count() < $chunkSize) { + return; + } + + $lastId = $results->last()->{$alias}; + } + }); + } + /** * Execute the query and get the first result. * @@ -147,6 +242,30 @@ public function first($columns = ['*']) return $this->take(1)->get($columns)->first(); } + /** + * Execute the query and get the first result if it's the sole matching record. + * + * @param array|string $columns + * @return \Illuminate\Database\Eloquent\Model|object|static|null + * + * @throws \Illuminate\Database\RecordsNotFoundException + * @throws \Illuminate\Database\MultipleRecordsFoundException + */ + public function sole($columns = ['*']) + { + $result = $this->take(2)->get($columns); + + if ($result->isEmpty()) { + throw new RecordsNotFoundException; + } + + if ($result->count() > 1) { + throw new MultipleRecordsFoundException; + } + + return $result->first(); + } + /** * Apply the callback's query changes if the given "value" is true. * diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index b4fa6d4c3a0d..b2ded4c9aebb 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -867,6 +867,16 @@ public function recordsHaveBeenModified($value = true) } } + /** + * Reset the record modification state. + * + * @return void + */ + public function forgetRecordModificationState() + { + $this->recordsModified = false; + } + /** * Is Doctrine available? * diff --git a/src/Illuminate/Database/Console/DbCommand.php b/src/Illuminate/Database/Console/DbCommand.php index ee6633557017..9152d1dc844c 100644 --- a/src/Illuminate/Database/Console/DbCommand.php +++ b/src/Illuminate/Database/Console/DbCommand.php @@ -83,7 +83,7 @@ public function commandEnvironment(array $connection) { $driver = ucfirst($connection['driver']); - if (method_exists($this, "get{$driver}Env")) { + if (method_exists($this, "get{$driver}Environment")) { return $this->{"get{$driver}Environment"}($connection); } diff --git a/src/Illuminate/Database/Console/DumpCommand.php b/src/Illuminate/Database/Console/DumpCommand.php index e7b60c7efa7c..fe73fb2af033 100644 --- a/src/Illuminate/Database/Console/DumpCommand.php +++ b/src/Illuminate/Database/Console/DumpCommand.php @@ -66,7 +66,7 @@ public function handle(ConnectionResolverInterface $connections, Dispatcher $dis protected function schemaState(Connection $connection) { return $connection->getSchemaState() - ->withMigrationTable(Config::get('database.migrations', 'migrations')) + ->withMigrationTable($connection->getTablePrefix().Config::get('database.migrations', 'migrations')) ->handleOutputUsing(function ($type, $buffer) { $this->output->write($buffer); }); diff --git a/src/Illuminate/Database/Console/Seeds/SeedCommand.php b/src/Illuminate/Database/Console/Seeds/SeedCommand.php index ccca6fd5eeda..058e545c234f 100644 --- a/src/Illuminate/Database/Console/Seeds/SeedCommand.php +++ b/src/Illuminate/Database/Console/Seeds/SeedCommand.php @@ -6,6 +6,7 @@ use Illuminate\Console\ConfirmableTrait; use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; class SeedCommand extends Command @@ -81,7 +82,7 @@ public function handle() */ protected function getSeeder() { - $class = $this->input->getOption('class'); + $class = $this->input->getArgument('class') ?? $this->input->getOption('class'); if (strpos($class, '\\') === false) { $class = 'Database\\Seeders\\'.$class; @@ -109,6 +110,18 @@ protected function getDatabase() return $database ?: $this->laravel['config']['database.default']; } + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['class', InputArgument::OPTIONAL, 'The class name of the root seeder', null], + ]; + } + /** * Get the console command options. * diff --git a/src/Illuminate/Database/DatabaseServiceProvider.php b/src/Illuminate/Database/DatabaseServiceProvider.php index 72f131d0db49..9f2ab18503e1 100755 --- a/src/Illuminate/Database/DatabaseServiceProvider.php +++ b/src/Illuminate/Database/DatabaseServiceProvider.php @@ -123,7 +123,9 @@ protected function registerDoctrineTypes() $types = $this->app['config']->get('database.dbal.types', []); foreach ($types as $name => $class) { - Type::addType($name, $class); + if (! Type::hasType($name)) { + Type::addType($name, $class); + } } } } diff --git a/src/Illuminate/Database/DatabaseTransactionRecord.php b/src/Illuminate/Database/DatabaseTransactionRecord.php index b4556d8fc305..3259552dcfbb 100755 --- a/src/Illuminate/Database/DatabaseTransactionRecord.php +++ b/src/Illuminate/Database/DatabaseTransactionRecord.php @@ -30,7 +30,7 @@ class DatabaseTransactionRecord * * @param string $connection * @param int $level - * @retunr void + * @return void */ public function __construct($connection, $level) { diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index c214c0396c84..191eefedc891 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -44,9 +44,14 @@ protected function causedByLostConnection(Throwable $e) 'running with the --read-only option so it cannot execute this statement', 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', 'SQLSTATE[HY000] [2002] Connection timed out', 'SSL: Connection timed out', + 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', + 'Temporary failure in name resolution', + 'SSL: Broken pipe', + 'SQLSTATE[08S01]: Communication link failure', ]); } } diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index d3f1f96a8c13..ca84d06cdbc1 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder as QueryBuilder; +use Illuminate\Database\RecordsNotFoundException; use Illuminate\Pagination\Paginator; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -25,7 +26,10 @@ */ class Builder { - use BuildsQueries, Concerns\QueriesRelationships, ExplainsQueries, ForwardsCalls; + use Concerns\QueriesRelationships, ExplainsQueries, ForwardsCalls; + use BuildsQueries { + sole as baseSole; + } /** * The base query builder instance. @@ -75,8 +79,25 @@ class Builder * @var string[] */ protected $passthru = [ - 'insert', 'insertOrIgnore', 'insertGetId', 'insertUsing', 'getBindings', 'toSql', 'dump', 'dd', - 'exists', 'doesntExist', 'count', 'min', 'max', 'avg', 'average', 'sum', 'getConnection', 'raw', 'getGrammar', + 'average', + 'avg', + 'count', + 'dd', + 'doesntExist', + 'dump', + 'exists', + 'getBindings', + 'getConnection', + 'getGrammar', + 'insert', + 'insertGetId', + 'insertOrIgnore', + 'insertUsing', + 'max', + 'min', + 'raw', + 'sum', + 'toSql', ]; /** @@ -504,6 +525,24 @@ public function firstOr($columns = ['*'], Closure $callback = null) return $callback(); } + /** + * Execute the query and get the first result if it's the sole matching record. + * + * @param array|string $columns + * @return \Illuminate\Database\Eloquent\Model + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Illuminate\Database\MultipleRecordsFoundException + */ + public function sole($columns = ['*']) + { + try { + return $this->baseSole($columns); + } catch (RecordsNotFoundException $exception) { + throw (new ModelNotFoundException)->setModel(get_class($this->model)); + } + } + /** * Get a single column's value from the first result of a query. * diff --git a/src/Illuminate/Database/Eloquent/Casts/ArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/ArrayObject.php new file mode 100644 index 000000000000..596ed836006b --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/ArrayObject.php @@ -0,0 +1,40 @@ +getArrayCopy()); + } + + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray() + { + return $this->getArrayCopy(); + } + + /** + * Get the array that should be JSON serialized. + * + * @return array + */ + public function jsonSerialize() + { + return $this->getArrayCopy(); + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php new file mode 100644 index 000000000000..a939e8acdbdb --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php @@ -0,0 +1,35 @@ + json_encode($value)]; + } + + public function serialize($model, string $key, $value, array $attributes) + { + return $value->getArrayCopy(); + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php new file mode 100644 index 000000000000..c2d567b504f7 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php @@ -0,0 +1,31 @@ + json_encode($value)]; + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php new file mode 100644 index 000000000000..b14fa8123e9b --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php @@ -0,0 +1,36 @@ + Crypt::encryptString(json_encode($value))]; + } + + public function serialize($model, string $key, $value, array $attributes) + { + return $value->getArrayCopy(); + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php new file mode 100644 index 000000000000..bb4f288d4196 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php @@ -0,0 +1,32 @@ + Crypt::encryptString(json_encode($value))]; + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index 9cbee56764ea..60b510cede60 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -130,7 +130,7 @@ public static function reguard() } /** - * Determine if current state is "unguarded". + * Determine if the current state is "unguarded". * * @return bool */ diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 0bde3e119227..459b14c73399 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -779,10 +779,14 @@ public function fillJsonAttribute($key, $value) { [$key, $path] = explode('->', $key, 2); - $this->attributes[$key] = $this->asJson($this->getArrayAttributeWithValue( + $value = $this->asJson($this->getArrayAttributeWithValue( $path, $key, $value )); + $this->attributes[$key] = $this->isEncryptedCastable($key) + ? $this->castAttributeAsEncryptedString($key, $value) + : $value; + return $this; } @@ -844,8 +848,15 @@ protected function getArrayAttributeWithValue($path, $key, $value) */ protected function getArrayAttributeByKey($key) { - return isset($this->attributes[$key]) ? - $this->fromJson($this->attributes[$key]) : []; + if (! isset($this->attributes[$key])) { + return []; + } + + return $this->fromJson( + $this->isEncryptedCastable($key) + ? $this->fromEncryptedString($this->attributes[$key]) + : $this->attributes[$key] + ); } /** @@ -1514,7 +1525,7 @@ protected function hasChanges($changes, $attributes = null) } /** - * Get the attributes that have been changed since last sync. + * Get the attributes that have been changed since the last sync. * * @return array */ diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 9ee9e3d124e3..5262d4305273 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -256,7 +256,7 @@ public function morphTo($name = null, $type = null, $id = null, $ownerKey = null // If the type value is null it is probably safe to assume we're eager loading // the relationship. In this case we'll just pass in a dummy query where we // need to remove any eager loads that may already be defined on a model. - return is_null($class = $this->getAttributeFromArray($type)) + return is_null($class = $this->getAttributeFromArray($type)) || $class === '' ? $this->morphEagerTo($name, $type, $id, $ownerKey) : $this->morphInstanceTo($class, $name, $type, $id, $ownerKey); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 4fbc2f90ebd1..7456fc6e4a6f 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -223,7 +223,7 @@ public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boole }; } - $query->where($this->query->from.'.'.$relation->getMorphType(), '=', (new $type)->getMorphClass()) + $query->where($this->qualifyColumn($relation->getMorphType()), '=', (new $type)->getMorphClass()) ->whereHas($belongsTo, $callback, $operator, $count); }); } diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 36f3dad84a8a..6a890646c623 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -5,17 +5,20 @@ use Closure; use Faker\Generator; use Illuminate\Container\Container; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Application; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; +use Illuminate\Support\Traits\Macroable; use Throwable; abstract class Factory { - use ForwardsCalls; + use ForwardsCalls, Macroable { + __call as macroCall; + } /** * The name of the factory's corresponding model. @@ -747,6 +750,10 @@ protected static function appNamespace() */ public function __call($method, $parameters) { + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); + } + if (! Str::startsWith($method, ['for', 'has'])) { static::throwBadMethodCallException($method); } diff --git a/src/Illuminate/Database/Eloquent/Factories/Sequence.php b/src/Illuminate/Database/Eloquent/Factories/Sequence.php index 20c0f3357c68..545a248f6362 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Sequence.php +++ b/src/Illuminate/Database/Eloquent/Factories/Sequence.php @@ -48,7 +48,7 @@ public function __invoke() $this->index = 0; } - return tap($this->sequence[$this->index], function () { + return tap(value($this->sequence[$this->index]), function () { $this->index = $this->index + 1; }); } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 528f9ca497a0..62fcd058e258 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\ConnectionResolverInterface as Resolver; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; use Illuminate\Database\Eloquent\Relations\HasManyThrough; @@ -1061,6 +1062,10 @@ protected function insertAndSetId(Builder $query, $attributes) */ public static function destroy($ids) { + if ($ids instanceof EloquentCollection) { + $ids = $ids->modelKeys(); + } + if ($ids instanceof BaseCollection) { $ids = $ids->all(); } @@ -1131,7 +1136,7 @@ public function delete() /** * Force a hard delete on a soft deleted model. * - * This method protects developers from running forceDelete when trait is missing. + * This method protects developers from running forceDelete when the trait is missing. * * @return bool|null */ diff --git a/src/Illuminate/Database/Eloquent/ModelNotFoundException.php b/src/Illuminate/Database/Eloquent/ModelNotFoundException.php index 2795b934bb74..c35598bdbf46 100755 --- a/src/Illuminate/Database/Eloquent/ModelNotFoundException.php +++ b/src/Illuminate/Database/Eloquent/ModelNotFoundException.php @@ -2,10 +2,10 @@ namespace Illuminate\Database\Eloquent; +use Illuminate\Database\RecordsNotFoundException; use Illuminate\Support\Arr; -use RuntimeException; -class ModelNotFoundException extends RuntimeException +class ModelNotFoundException extends RecordsNotFoundException { /** * Name of the affected Eloquent model. diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php index ce7ba4421973..a98cba0ad375 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php @@ -6,11 +6,14 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; class BelongsTo extends Relation { - use ComparesRelatedModels, SupportsDefaultModels; + use ComparesRelatedModels, + InteractsWithDictionary, + SupportsDefaultModels; /** * The child model instance of the relation. @@ -174,15 +177,19 @@ public function match(array $models, Collection $results, $relation) $dictionary = []; foreach ($results as $result) { - $dictionary[$result->getAttribute($owner)] = $result; + $attribute = $this->getDictionaryKey($result->getAttribute($owner)); + + $dictionary[$attribute] = $result; } // Once we have the dictionary constructed, we can loop through all the parents // and match back onto their children using these keys of the dictionary and // the primary key of the children to map them onto the correct instances. foreach ($models as $model) { - if (isset($dictionary[$model->{$foreign}])) { - $model->setRelation($relation, $dictionary[$model->{$foreign}]); + $attribute = $this->getDictionaryKey($model->{$foreign}); + + if (isset($dictionary[$attribute])) { + $model->setRelation($relation, $dictionary[$attribute]); } } diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 03a47861643c..d85030b9a132 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -8,12 +8,14 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable; use Illuminate\Support\Str; use InvalidArgumentException; class BelongsToMany extends Relation { - use Concerns\InteractsWithPivotTable; + use InteractsWithDictionary, InteractsWithPivotTable; /** * The intermediate table for the relation. @@ -277,7 +279,9 @@ public function match(array $models, Collection $results, $relation) // children back to their parent using the dictionary and the keys on the // the parent models. Then we will return the hydrated models back out. foreach ($models as $model) { - if (isset($dictionary[$key = $model->{$this->parentKey}])) { + $key = $this->getDictionaryKey($model->{$this->parentKey}); + + if (isset($dictionary[$key])) { $model->setRelation( $relation, $this->related->newCollection($dictionary[$key]) ); @@ -301,7 +305,9 @@ protected function buildDictionary(Collection $results) $dictionary = []; foreach ($results as $result) { - $dictionary[$result->{$this->accessor}->{$this->foreignPivotKey}][] = $result; + $value = $this->getDictionaryKey($result->{$this->accessor}->{$this->foreignPivotKey}); + + $dictionary[$value][] = $result; } return $dictionary; @@ -569,7 +575,7 @@ public function orderByPivot($column, $direction = 'asc') } /** - * Find a related model by its primary key or return new instance of the related model. + * Find a related model by its primary key or return a new instance of the related model. * * @param mixed $id * @param array $columns @@ -865,9 +871,7 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p */ public function chunk($count, callable $callback) { - $this->query->addSelect($this->shouldSelect()); - - return $this->query->chunk($count, function ($results, $page) use ($callback) { + return $this->prepareQueryBuilder()->chunk($count, function ($results, $page) use ($callback) { $this->hydratePivotRelation($results->all()); return $callback($results, $page); @@ -885,7 +889,7 @@ public function chunk($count, callable $callback) */ public function chunkById($count, callable $callback, $column = null, $alias = null) { - $this->query->addSelect($this->shouldSelect()); + $this->prepareQueryBuilder(); $column = $column ?? $this->getRelated()->qualifyColumn( $this->getRelatedKeyName() @@ -918,6 +922,44 @@ public function each(callable $callback, $count = 1000) }); } + /** + * Query lazily, by chunks of the given size. + * + * @param int $chunkSize + * @return \Illuminate\Support\LazyCollection + */ + public function lazy($chunkSize = 1000) + { + return $this->prepareQueryBuilder()->lazy($chunkSize)->map(function ($model) { + $this->hydratePivotRelation([$model]); + + return $model; + }); + } + + /** + * Query lazily, by chunking the results of a query by comparing IDs. + * + * @param int $count + * @param string|null $column + * @param string|null $alias + * @return \Illuminate\Support\LazyCollection + */ + public function lazyById($chunkSize = 1000, $column = null, $alias = null) + { + $column = $column ?? $this->getRelated()->qualifyColumn( + $this->getRelatedKeyName() + ); + + $alias = $alias ?? $this->getRelatedKeyName(); + + return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias)->map(function ($model) { + $this->hydratePivotRelation([$model]); + + return $model; + }); + } + /** * Get a lazy collection for the given query. * @@ -925,15 +967,23 @@ public function each(callable $callback, $count = 1000) */ public function cursor() { - $this->query->addSelect($this->shouldSelect()); - - return $this->query->cursor()->map(function ($model) { + return $this->prepareQueryBuilder()->cursor()->map(function ($model) { $this->hydratePivotRelation([$model]); return $model; }); } + /** + * Prepare the query builder for query execution. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function prepareQueryBuilder() + { + return $this->query->addSelect($this->shouldSelect()); + } + /** * Hydrate the pivot table relationship on the models. * diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php new file mode 100644 index 000000000000..9e2186150630 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php @@ -0,0 +1,27 @@ +__toString(); + } + + throw new InvalidArgumentException('Model attribute value is an object but does not have a __toString method.'); + } + + return $attribute; + } +} diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 117eed2f8aeb..512ddd0cae15 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -446,7 +446,7 @@ public function detach($ids = null, $touch = true) return 0; } - $query->whereIn($this->relatedPivotKey, (array) $ids); + $query->whereIn($this->getQualifiedRelatedPivotKeyName(), (array) $ids); } // Once we have all of the conditions set on the statement, we are ready @@ -567,7 +567,7 @@ public function newPivotQuery() $query->whereNull(...$arguments); } - return $query->where($this->foreignPivotKey, $this->parent->{$this->parentKey}); + return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey}); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index 6285cf230b7f..9ea307562ca1 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -7,10 +7,13 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\SoftDeletes; class HasManyThrough extends Relation { + use InteractsWithDictionary; + /** * The "through" parent model instance. * @@ -193,7 +196,7 @@ public function match(array $models, Collection $results, $relation) // link them up with their children using the keyed dictionary to make the // matching very convenient and easy work. Then we'll just return them. foreach ($models as $model) { - if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { $model->setRelation( $relation, $this->related->newCollection($dictionary[$key]) ); @@ -501,6 +504,34 @@ public function each(callable $callback, $count = 1000) }); } + /** + * Query lazily, by chunks of the given size. + * + * @param int $chunkSize + * @return \Illuminate\Support\LazyCollection + */ + public function lazy($chunkSize = 1000) + { + return $this->prepareQueryBuilder()->lazy($chunkSize); + } + + /** + * Query lazily, by chunking the results of a query by comparing IDs. + * + * @param int $count + * @param string|null $column + * @param string|null $alias + * @return \Illuminate\Support\LazyCollection + */ + public function lazyById($chunkSize = 1000, $column = null, $alias = null) + { + $column = $column ?? $this->getRelated()->getQualifiedKeyName(); + + $alias = $alias ?? $this->getRelated()->getKeyName(); + + return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias); + } + /** * Prepare the query builder for query execution. * diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index ede942018f6f..18b0f8fc9256 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -5,9 +5,12 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; abstract class HasOneOrMany extends Relation { + use InteractsWithDictionary; + /** * The foreign key of the parent model. * @@ -53,7 +56,7 @@ public function make(array $attributes = []) } /** - * Create and return an un-saved instances of the related models. + * Create and return an un-saved instance of the related models. * * @param iterable $records * @return \Illuminate\Database\Eloquent\Collection @@ -141,7 +144,7 @@ protected function matchOneOrMany(array $models, Collection $results, $relation, // link them up with their children using the keyed dictionary to make the // matching very convenient and easy work. Then we'll just return them. foreach ($models as $model) { - if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { $model->setRelation( $relation, $this->getRelationValue($dictionary, $key, $type) ); @@ -177,12 +180,12 @@ protected function buildDictionary(Collection $results) $foreign = $this->getForeignKeyName(); return $results->mapToDictionary(function ($result) use ($foreign) { - return [$result->{$foreign} => $result]; + return [$this->getDictionaryKey($result->{$foreign}) => $result]; })->all(); } /** - * Find a model by its primary key or return new instance of the related model. + * Find a model by its primary key or return a new instance of the related model. * * @param mixed $id * @param array $columns diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasOneThrough.php index a48c3186214a..ed9c7baa4dc3 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneThrough.php @@ -4,11 +4,12 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; class HasOneThrough extends HasManyThrough { - use SupportsDefaultModels; + use InteractsWithDictionary, SupportsDefaultModels; /** * Get the results of the relationship. @@ -52,7 +53,7 @@ public function match(array $models, Collection $results, $relation) // link them up with their children using the keyed dictionary to make the // matching very convenient and easy work. Then we'll just return them. foreach ($models as $model) { - if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { + if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) { $value = $dictionary[$key]; $model->setRelation( $relation, reset($value) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php index 314d61fc7f36..7fbe484aac99 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php @@ -74,6 +74,16 @@ public function delete() }); } + /** + * Get the morph type for the pivot. + * + * @return string + */ + public function getMorphType() + { + return $this->morphType; + } + /** * Set the morph type for the pivot. * diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index c9db5d2b2c6c..53d25cb4470a 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -6,9 +6,12 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; class MorphTo extends BelongsTo { + use InteractsWithDictionary; + /** * The type of the polymorphic relation. * @@ -97,7 +100,10 @@ protected function buildDictionary(Collection $models) { foreach ($models as $model) { if ($model->{$this->morphType}) { - $this->dictionary[$model->{$this->morphType}][$model->{$this->foreignKey}][] = $model; + $morphTypeKey = $this->getDictionaryKey($model->{$this->morphType}); + $foreignKeyKey = $this->getDictionaryKey($model->{$this->foreignKey}); + + $this->dictionary[$morphTypeKey][$foreignKeyKey][] = $model; } } } @@ -164,7 +170,7 @@ protected function gatherKeysByType($type, $keyType) ? array_keys($this->dictionary[$type]) : array_map(function ($modelId) { return (string) $modelId; - }, array_keys($this->dictionary[$type])); + }, array_filter(array_keys($this->dictionary[$type]))); } /** @@ -207,7 +213,7 @@ public function match(array $models, Collection $results, $relation) protected function matchToMorphParents($type, Collection $results) { foreach ($results as $result) { - $ownerKey = ! is_null($this->ownerKey) ? $result->{$this->ownerKey} : $result->getKey(); + $ownerKey = ! is_null($this->ownerKey) ? $this->getDictionaryKey($result->{$this->ownerKey}) : $result->getKey(); if (isset($this->dictionary[$type][$ownerKey])) { foreach ($this->dictionary[$type][$ownerKey] as $model) { @@ -226,7 +232,7 @@ protected function matchToMorphParents($type, Collection $results) public function associate($model) { $this->parent->setAttribute( - $this->foreignKey, $model instanceof Model ? $model->getKey() : null + $this->foreignKey, $model instanceof Model ? $model->{$this->ownerKey ?: $model->getKeyName()} : null ); $this->parent->setAttribute( @@ -324,7 +330,7 @@ public function morphWithCount(array $withCount) } /** - * Specify constraints on the query for a given morph types. + * Specify constraints on the query for a given morph type. * * @param array $callbacks * @return \Illuminate\Database\Eloquent\Relations\MorphTo diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 3fcb75e67ab5..29131b275e3d 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; use Illuminate\Support\Traits\ForwardsCalls; @@ -49,7 +51,7 @@ abstract class Relation protected static $constraints = true; /** - * An array to map class names to their morph names in database. + * An array to map class names to their morph names in the database. * * @var array */ @@ -151,6 +153,30 @@ public function getEager() return $this->get(); } + /** + * Execute the query and get the first result if it's the sole matching record. + * + * @param array|string $columns + * @return \Illuminate\Database\Eloquent\Model + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Illuminate\Database\MultipleRecordsFoundException + */ + public function sole($columns = ['*']) + { + $result = $this->take(2)->get($columns); + + if ($result->isEmpty()) { + throw (new ModelNotFoundException)->setModel(get_class($this->related)); + } + + if ($result->count() > 1) { + throw new MultipleRecordsFoundException; + } + + return $result->first(); + } + /** * Execute the query as a "select" statement. * @@ -223,7 +249,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, /** * Get a relationship join table hash. * - * @param bool $incrementJoinCount + * @param bool $incrementJoinCount * @return string */ public function getRelationCountHash($incrementJoinCount = true) diff --git a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php index 9d5a134409f0..840a5e1dfce1 100755 --- a/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php +++ b/src/Illuminate/Database/Migrations/MigrationRepositoryInterface.php @@ -12,7 +12,7 @@ interface MigrationRepositoryInterface public function getRan(); /** - * Get list of migrations. + * Get the list of migrations. * * @param int $steps * @return array diff --git a/src/Illuminate/Database/MultipleRecordsFoundException.php b/src/Illuminate/Database/MultipleRecordsFoundException.php new file mode 100755 index 000000000000..cccb7e4177bf --- /dev/null +++ b/src/Illuminate/Database/MultipleRecordsFoundException.php @@ -0,0 +1,10 @@ +addBinding($value, 'where'); + $this->addBinding($this->flattenValue($value), 'where'); } return $this; @@ -1121,7 +1121,7 @@ public function whereBetween($column, array $values, $boolean = 'and', $not = fa $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not'); - $this->addBinding($this->cleanBindings($values), 'where'); + $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'where'); return $this; } @@ -1244,6 +1244,8 @@ public function whereDate($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = $this->flattenValue($value); + if ($value instanceof DateTimeInterface) { $value = $value->format('Y-m-d'); } @@ -1283,6 +1285,8 @@ public function whereTime($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = $this->flattenValue($value); + if ($value instanceof DateTimeInterface) { $value = $value->format('H:i:s'); } @@ -1322,6 +1326,8 @@ public function whereDay($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = $this->flattenValue($value); + if ($value instanceof DateTimeInterface) { $value = $value->format('d'); } @@ -1365,6 +1371,8 @@ public function whereMonth($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = $this->flattenValue($value); + if ($value instanceof DateTimeInterface) { $value = $value->format('m'); } @@ -1408,6 +1416,8 @@ public function whereYear($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = $this->flattenValue($value); + if ($value instanceof DateTimeInterface) { $value = $value->format('Y'); } @@ -1716,7 +1726,7 @@ public function whereJsonLength($column, $operator, $value = null, $boolean = 'a $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); if (! $value instanceof Expression) { - $this->addBinding($value); + $this->addBinding((int) $this->flattenValue($value)); } return $this; @@ -1865,7 +1875,7 @@ public function having($column, $operator = null, $value = null, $boolean = 'and $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); if (! $value instanceof Expression) { - $this->addBinding($value, 'having'); + $this->addBinding($this->flattenValue($value), 'having'); } return $this; @@ -1903,7 +1913,7 @@ public function havingBetween($column, array $values, $boolean = 'and', $not = f $this->havings[] = compact('type', 'column', 'values', 'boolean', 'not'); - $this->addBinding($this->cleanBindings($values), 'having'); + $this->addBinding(array_slice($this->cleanBindings(Arr::flatten($values)), 0, 2), 'having'); return $this; } @@ -3168,6 +3178,17 @@ public function cleanBindings(array $bindings) })); } + /** + * Get a scalar type value from an unknown type of input. + * + * @param mixed $value + * @return mixed + */ + protected function flattenValue($value) + { + return is_array($value) ? head(Arr::flatten($value)) : $value; + } + /** * Get the default key name of the table. * diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 8b0a3b311b78..b7305e8ea382 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -459,7 +459,7 @@ protected function dateBasedWhere($type, Builder $query, $where) } /** - * Compile a where clause comparing two columns.. + * Compile a where clause comparing two columns. * * @param \Illuminate\Database\Query\Builder $query * @param array $where diff --git a/src/Illuminate/Database/RecordsNotFoundException.php b/src/Illuminate/Database/RecordsNotFoundException.php new file mode 100755 index 000000000000..3e0d9557581d --- /dev/null +++ b/src/Illuminate/Database/RecordsNotFoundException.php @@ -0,0 +1,10 @@ +columns[] = $column = new ForeignIdColumnDefinition($this, [ + return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [ 'type' => 'bigInteger', 'name' => $column, 'autoIncrement' => false, 'unsigned' => true, - ]); - - return $column; + ])); } /** @@ -1189,10 +1194,10 @@ public function uuid($column) */ public function foreignUuid($column) { - return $this->columns[] = new ForeignIdColumnDefinition($this, [ + return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [ 'type' => 'uuid', 'name' => $column, - ]); + ])); } /** @@ -1504,11 +1509,44 @@ protected function createIndexName($type, array $columns) */ public function addColumn($type, $name, array $parameters = []) { - $this->columns[] = $column = new ColumnDefinition( + return $this->addColumnDefinition(new ColumnDefinition( array_merge(compact('type', 'name'), $parameters) - ); + )); + } + + /** + * Add a new column definition to the blueprint. + * + * @param \Illuminate\Database\Schema\ColumnDefinition $definition + * @return \Illuminate\Database\Schema\ColumnDefinition + */ + protected function addColumnDefinition($definition) + { + $this->columns[] = $definition; + + if ($this->after) { + $definition->after($this->after); + + $this->after = $definition->name; + } + + return $definition; + } + + /** + * Add the columns from the callback after the given column. + * + * @param string $column + * @param \Closure $callback + * @return void + */ + public function after($column, Closure $callback) + { + $this->after = $column; + + $callback($this); - return $column; + $this->after = null; } /** @@ -1607,7 +1645,7 @@ public function getChangedColumns() } /** - * Determine if the blueprint has auto increment columns. + * Determine if the blueprint has auto-increment columns. * * @return bool */ @@ -1619,7 +1657,7 @@ public function hasAutoIncrementColumn() } /** - * Get the auto increment column starting values. + * Get the auto-increment column starting values. * * @return array */ diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 80611bfc92ed..04f96e43308a 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -94,6 +94,28 @@ public static function morphUsingUuids() return static::defaultMorphKeyType('uuid'); } + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + throw new LogicException('This database driver does not support creating databases.'); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + throw new LogicException('This database driver does not support dropping databases.'); + } + /** * Determine if the given table exists. * diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index b60dfe817b62..18071b2fbb12 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -9,6 +9,7 @@ use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Fluent; +use LogicException; use RuntimeException; abstract class Grammar extends BaseGrammar @@ -27,6 +28,29 @@ abstract class Grammar extends BaseGrammar */ protected $fluentCommands = []; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + throw new LogicException('This database driver does not support creating databases.'); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + throw new LogicException('This database driver does not support dropping databases.'); + } + /** * Compile a rename column command. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index c2952e47926c..c1a2c5586b54 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -26,6 +26,37 @@ class MySqlGrammar extends Grammar */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + 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')), + ); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + return sprintf( + 'drop database if exists %s', + $this->wrapValue($name) + ); + } + /** * Compile the query to determine the list of tables. * @@ -160,7 +191,7 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) } /** - * Compile the auto incrementing column starting values. + * Compile the auto-incrementing column starting values. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return array diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 204beecea3b2..adaf21f90e4c 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -35,6 +35,36 @@ class PostgresGrammar extends Grammar */ protected $fluentCommands = ['Comment']; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + return sprintf( + 'create database %s encoding %s', + $this->wrapValue($name), + $this->wrapValue($connection->getConfig('charset')), + ); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + return sprintf( + 'drop database if exists %s', + $this->wrapValue($name) + ); + } + /** * Compile the query to determine if a table exists. * @@ -87,7 +117,7 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) } /** - * Compile the auto incrementing column starting values. + * Compile the auto-incrementing column starting values. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return array diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 5075c0504204..556d749e23b2 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -875,7 +875,7 @@ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) protected function modifyNullable(Blueprint $blueprint, Fluent $column) { if (is_null($column->virtualAs) && is_null($column->storedAs)) { - return $column->nullable ? ' null' : ' not null'; + return $column->nullable ? '' : ' not null'; } if ($column->nullable === false) { diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 79f3f0f5e3ad..c3fc442e2368 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -28,6 +28,35 @@ class SqlServerGrammar extends Grammar */ protected $serials = ['tinyInteger', 'smallInteger', 'mediumInteger', 'integer', 'bigInteger']; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + return sprintf( + 'create database %s', + $this->wrapValue($name), + ); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + return sprintf( + 'drop database if exists %s', + $this->wrapValue($name) + ); + } + /** * Compile the query to determine if a table exists. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index f07946c85e23..b7cff5568d1b 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -4,6 +4,32 @@ class MySqlBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return $this->connection->statement( + $this->grammar->compileCreateDatabase($name, $this->connection) + ); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return $this->connection->statement( + $this->grammar->compileDropDatabaseIfExists($name) + ); + } + /** * Determine if the given table exists. * diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 2d46ebe32355..56a4ea455b5d 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -71,7 +71,9 @@ public function load($path) { $command = 'mysql '.$this->connectionString().' --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"'; - $this->makeProcess($command)->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + $process = $this->makeProcess($command)->setTimeout(null); + + $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, ])); } @@ -147,6 +149,12 @@ protected function executeDumpProcess(Process $process, $output, array $variable ), $output, $variables); } + if (Str::contains($e->getMessage(), ['set-gtid-purged'])) { + return $this->executeDumpProcess(Process::fromShellCommandLine( + str_replace(' --set-gtid-purged=OFF', '', $process->getCommandLine()) + ), $output, $variables); + } + throw $e; } diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 76673a719a41..82702a802691 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -4,6 +4,32 @@ class PostgresBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return $this->connection->statement( + $this->grammar->compileCreateDatabase($name, $this->connection) + ); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return $this->connection->statement( + $this->grammar->compileDropDatabaseIfExists($name) + ); + } + /** * Determine if the given table exists. * diff --git a/src/Illuminate/Database/Schema/PostgresSchemaState.php b/src/Illuminate/Database/Schema/PostgresSchemaState.php index 179db6189c55..c844ec542e01 100644 --- a/src/Illuminate/Database/Schema/PostgresSchemaState.php +++ b/src/Illuminate/Database/Schema/PostgresSchemaState.php @@ -21,7 +21,7 @@ public function dump(Connection $connection, $path) ->reject(function ($table) { return $table === $this->migrationTable; })->map(function ($table) { - return '--exclude-table-data='.$table; + return '--exclude-table-data="*.'.$table.'"'; })->implode(' '); $this->makeProcess( @@ -39,7 +39,7 @@ public function dump(Connection $connection, $path) */ public function load($path) { - $command = 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_restore --no-owner --no-acl --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE $LARAVEL_LOAD_PATH'; + $command = 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_restore --no-owner --no-acl --clean --if-exists --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE $LARAVEL_LOAD_PATH'; if (Str::endsWith($path, '.sql')) { $command = 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD psql --file=$LARAVEL_LOAD_PATH --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE'; diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 78b6b9c78d2e..6a1dbae23ec6 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -2,8 +2,34 @@ namespace Illuminate\Database\Schema; +use Illuminate\Support\Facades\File; + class SQLiteBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return File::put($name, '') !== false; + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return File::exists($name) + ? File::delete($name) + : true; + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/Schema/SqlServerBuilder.php b/src/Illuminate/Database/Schema/SqlServerBuilder.php index 0b3e47bec9a6..223abd44ed1c 100644 --- a/src/Illuminate/Database/Schema/SqlServerBuilder.php +++ b/src/Illuminate/Database/Schema/SqlServerBuilder.php @@ -4,6 +4,32 @@ class SqlServerBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return $this->connection->statement( + $this->grammar->compileCreateDatabase($name, $this->connection) + ); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return $this->connection->statement( + $this->grammar->compileDropDatabaseIfExists($name) + ); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index b0b8490d062a..87628b99cc9f 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -43,7 +43,7 @@ public function transaction(Closure $callback, $attempts = 1) $this->getPdo()->exec('COMMIT TRAN'); } - // If we catch an exception, we will roll back so nothing gets messed + // If we catch an exception, we will rollback so nothing gets messed // up in the database. Then we'll re-throw the exception so it can // be handled how the developer sees fit for their applications. catch (Throwable $e) { diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 8159b2bf6a9e..c0e5e50b8092 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -5,9 +5,10 @@ use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract; use Illuminate\Contracts\Encryption\EncryptException; +use Illuminate\Contracts\Encryption\StringEncrypter; use RuntimeException; -class Encrypter implements EncrypterContract +class Encrypter implements EncrypterContract, StringEncrypter { /** * The encryption key. diff --git a/src/Illuminate/Events/CallQueuedListener.php b/src/Illuminate/Events/CallQueuedListener.php index 90ad3d9ac459..9201d858613b 100644 --- a/src/Illuminate/Events/CallQueuedListener.php +++ b/src/Illuminate/Events/CallQueuedListener.php @@ -61,6 +61,13 @@ class CallQueuedListener implements ShouldQueue */ public $timeout; + /** + * Indicates if the job should be encrypted. + * + * @var bool + */ + public $shouldBeEncrypted = false; + /** * Create a new job instance. * diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index dac103348a7d..96f08f01b514 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Contracts\Container\Container as ContainerContract; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -70,7 +71,7 @@ public function __construct(ContainerContract $container = null) * Register an event listener with the dispatcher. * * @param \Closure|string|array $events - * @param \Closure|string|null $listener + * @param \Closure|string|array|null $listener * @return void */ public function listen($events, $listener = null) @@ -285,7 +286,7 @@ protected function shouldBroadcast(array $payload) } /** - * Check if event should be broadcasted by condition. + * Check if the event should be broadcasted by the condition. * * @param mixed $event * @return bool @@ -600,6 +601,8 @@ protected function propagateListenerOptions($listener, $job) $job->retryUntil = method_exists($listener, 'retryUntil') ? $listener->retryUntil() : null; + + $job->shouldBeEncrypted = $listener instanceof ShouldBeEncrypted; }); } diff --git a/src/Illuminate/Events/NullDispatcher.php b/src/Illuminate/Events/NullDispatcher.php index dcfdc95f9e94..5c539d53a361 100644 --- a/src/Illuminate/Events/NullDispatcher.php +++ b/src/Illuminate/Events/NullDispatcher.php @@ -37,6 +37,7 @@ public function __construct(DispatcherContract $dispatcher) */ public function dispatch($event, $payload = [], $halt = false) { + // } /** @@ -48,6 +49,7 @@ public function dispatch($event, $payload = [], $halt = false) */ public function push($event, $payload = []) { + // } /** @@ -59,13 +61,14 @@ public function push($event, $payload = []) */ public function until($event, $payload = []) { + // } /** * Register an event listener with the dispatcher. * * @param \Closure|string|array $events - * @param \Closure|string|null $listener + * @param \Closure|string|array|null $listener * @return void */ public function listen($events, $listener = null) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 1c33b9892676..935c413cd6c0 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -20,8 +20,10 @@ use League\Flysystem\FileExistsException; use League\Flysystem\FileNotFoundException; use League\Flysystem\FilesystemInterface; +use League\Flysystem\Sftp\SftpAdapter as Sftp; use PHPUnit\Framework\Assert as PHPUnit; use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; use RuntimeException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -449,7 +451,7 @@ public function url($path) return $this->driver->getUrl($path); } elseif ($adapter instanceof AwsS3Adapter) { return $this->getAwsUrl($adapter, $path); - } elseif ($adapter instanceof Ftp) { + } elseif ($adapter instanceof Ftp || $adapter instanceof Sftp) { return $this->getFtpUrl($path); } elseif ($adapter instanceof LocalAdapter) { return $this->getLocalUrl($path); @@ -592,9 +594,18 @@ public function getAwsTemporaryUrl($adapter, $path, $expiration, $options) 'Key' => $adapter->getPathPrefix().$path, ], $options)); - return (string) $client->createPresignedRequest( + $uri = $client->createPresignedRequest( $command, $expiration )->getUri(); + + // If an explicit base URL has been set on the disk configuration then we will use + // it as the base URL instead of the default path. This allows the developer to + // have full control over the base path for this filesystem's generated URLs. + if (! is_null($url = $this->driver->getConfig()->get('temporary_url'))) { + $uri = $this->replaceBaseUrl($uri, $url); + } + + return (string) $uri; } /** @@ -609,6 +620,20 @@ protected function concatPathToUrl($url, $path) return rtrim($url, '/').'/'.ltrim($path, '/'); } + /** + * Replace the scheme and host of the given UriInterface with values from the given URL. + * + * @param \Psr\Http\Message\UriInterface $uri + * @param string $url + * @return \Psr\Http\Message\UriInterface + */ + protected function replaceBaseUrl($uri, $url) + { + $parsed = parse_url($url); + + return $uri->withScheme($parsed['scheme'])->withHost($parsed['host']); + } + /** * Get an array of all files in a directory. * diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index 5575439418fc..49768690dc24 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -125,11 +125,11 @@ protected function resolve($name) $driverMethod = 'create'.ucfirst($name).'Driver'; - if (method_exists($this, $driverMethod)) { - return $this->{$driverMethod}($config); - } else { + if (! method_exists($this, $driverMethod)) { throw new InvalidArgumentException("Driver [{$name}] is not supported."); } + + return $this->{$driverMethod}($config); } /** diff --git a/src/Illuminate/Filesystem/LockableFile.php b/src/Illuminate/Filesystem/LockableFile.php index edb801f4847b..a095d4e9a9b5 100644 --- a/src/Illuminate/Filesystem/LockableFile.php +++ b/src/Illuminate/Filesystem/LockableFile.php @@ -2,6 +2,7 @@ namespace Illuminate\Filesystem; +use Exception; use Illuminate\Contracts\Filesystem\LockTimeoutException; class LockableFile @@ -65,6 +66,10 @@ 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.'); + } } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index c7049c14096c..6a6487966b72 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -33,7 +33,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '8.20.1'; + const VERSION = '8.35.1'; /** * The base path for the Laravel installation. @@ -112,6 +112,13 @@ class Application extends Container implements ApplicationContract, CachesConfig */ protected $databasePath; + /** + * The custom language file path defined by the developer. + * + * @var string + */ + protected $langPath; + /** * The custom storage path defined by the developer. * @@ -407,7 +414,30 @@ public function useDatabasePath($path) */ public function langPath() { - return $this->resourcePath().DIRECTORY_SEPARATOR.'lang'; + if ($this->langPath) { + return $this->langPath; + } + + if (is_dir($path = $this->resourcePath().DIRECTORY_SEPARATOR.'lang')) { + return $path; + } + + return $this->basePath().DIRECTORY_SEPARATOR.'lang'; + } + + /** + * Set the language file directory. + * + * @param string $path + * @return $this + */ + public function useLangPath($path) + { + $this->langPath = $path; + + $this->instance('path.lang', $path); + + return $this; } /** @@ -456,6 +486,21 @@ public function resourcePath($path = '') return $this->basePath.DIRECTORY_SEPARATOR.'resources'.($path ? DIRECTORY_SEPARATOR.$path : $path); } + /** + * Get the path to the views directory. + * + * This method returns the first configured path in the array of view paths. + * + * @param string $path + * @return string + */ + public function viewPath($path = '') + { + $basePath = $this['config']->get('view.paths')[0]; + + return rtrim($basePath, DIRECTORY_SEPARATOR).($path ? DIRECTORY_SEPARATOR.$path : $path); + } + /** * Get the path to the environment file directory. * @@ -530,7 +575,7 @@ public function environment(...$environments) } /** - * Determine if application is in local environment. + * Determine if the application is in the local environment. * * @return bool */ @@ -540,7 +585,7 @@ public function isLocal() } /** - * Determine if application is in production environment. + * Determine if the application is in the production environment. * * @return bool */ @@ -1230,7 +1275,7 @@ public function setFallbackLocale($fallbackLocale) } /** - * Determine if application locale is the given locale. + * Determine if the application locale is the given locale. * * @param string $locale * @return bool @@ -1257,9 +1302,9 @@ public function registerCoreContainerAliases() 'cache.psr6' => [\Symfony\Component\Cache\Adapter\Psr16Adapter::class, \Symfony\Component\Cache\Adapter\AdapterInterface::class, \Psr\Cache\CacheItemPoolInterface::class], 'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class], 'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class], - 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class], 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], + 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\StringEncrypter::class], 'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], 'files' => [\Illuminate\Filesystem\Filesystem::class], 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], diff --git a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php index 0fe6964d5940..78873de065ee 100644 --- a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php +++ b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php @@ -131,8 +131,13 @@ protected function replaceModel($stub, $model) array_keys($replace), array_values($replace), $stub ); - return str_replace( - "use {$namespacedModel};\nuse {$namespacedModel};", "use {$namespacedModel};", $stub + return preg_replace( + vsprintf('/use %s;[\r\n]+use %s;/', [ + preg_quote($namespacedModel, '/'), + preg_quote($namespacedModel, '/'), + ]), + "use {$namespacedModel};", + $stub ); } diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index d14e58cf6315..8a24c5dfb8bc 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -67,6 +67,8 @@ public function __construct(Router $router) */ public function handle() { + $this->router->flushMiddlewareGroups(); + if (empty($this->router->getRoutes())) { return $this->error("Your application doesn't have any routes."); } @@ -163,7 +165,7 @@ protected function displayRoutes(array $routes) } /** - * Get before filters. + * Get the middleware for the route. * * @param \Illuminate\Routing\Route $route * @return string @@ -189,6 +191,14 @@ protected function filterRoute(array $route) return; } + if ($this->option('except-path')) { + foreach (explode(',', $this->option('except-path')) as $path) { + if (Str::contains($route['uri'], $path)) { + return; + } + } + } + return $route; } @@ -256,7 +266,8 @@ protected function getOptions() ['json', null, InputOption::VALUE_NONE, 'Output the route list as JSON'], ['method', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by method'], ['name', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by name'], - ['path', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by path'], + ['path', null, InputOption::VALUE_OPTIONAL, 'Only show routes matching the given path pattern'], + ['except-path', null, InputOption::VALUE_OPTIONAL, 'Do not display the routes matching the given path pattern'], ['reverse', 'r', InputOption::VALUE_NONE, 'Reverse the ordering of the routes'], ['sort', null, InputOption::VALUE_OPTIONAL, 'The column (domain, method, uri, name, action, middleware) to sort by', 'uri'], ]; diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index 20b7f59c8f7f..a41393179530 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -149,7 +149,7 @@ protected function port() } /** - * Check if command has reached its max amount of port tries. + * Check if the command has reached its max amount of port tries. * * @return bool */ diff --git a/src/Illuminate/Foundation/Console/StorageLinkCommand.php b/src/Illuminate/Foundation/Console/StorageLinkCommand.php index a0419bf6c077..0d47ddae7294 100644 --- a/src/Illuminate/Foundation/Console/StorageLinkCommand.php +++ b/src/Illuminate/Foundation/Console/StorageLinkCommand.php @@ -35,6 +35,10 @@ public function handle() continue; } + if (is_link($link)) { + $this->laravel->make('files')->delete($link); + } + if ($relative) { $this->laravel->make('files')->relativeLink($target, $link); } else { diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index 17a459e72834..501142f0d63c 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Foundation\Events\VendorTagPublished; use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; use League\Flysystem\Adapter\Local as LocalAdapter; @@ -159,7 +160,9 @@ protected function publishTag($tag) { $published = false; - foreach ($this->pathsToPublish($tag) as $from => $to) { + $pathsToPublish = $this->pathsToPublish($tag); + + foreach ($pathsToPublish as $from => $to) { $this->publishItem($from, $to); $published = true; @@ -167,6 +170,8 @@ protected function publishTag($tag) if ($published === false) { $this->error('Unable to locate publishable resources.'); + } else { + $this->laravel['events']->dispatch(new VendorTagPublished($tag, $pathsToPublish)); } } diff --git a/src/Illuminate/Foundation/Console/stubs/exception-render-report.stub b/src/Illuminate/Foundation/Console/stubs/exception-render-report.stub index bdba7d159430..4d1070c2f687 100644 --- a/src/Illuminate/Foundation/Console/stubs/exception-render-report.stub +++ b/src/Illuminate/Foundation/Console/stubs/exception-render-report.stub @@ -9,7 +9,7 @@ class DummyClass extends Exception /** * Report the exception. * - * @return bool|void + * @return bool|null */ public function report() { diff --git a/src/Illuminate/Foundation/Console/stubs/exception-report.stub b/src/Illuminate/Foundation/Console/stubs/exception-report.stub index 786e0d2915a3..643149863e91 100644 --- a/src/Illuminate/Foundation/Console/stubs/exception-report.stub +++ b/src/Illuminate/Foundation/Console/stubs/exception-report.stub @@ -9,7 +9,7 @@ class DummyClass extends Exception /** * Report the exception. * - * @return bool|void + * @return bool|null */ public function report() { diff --git a/src/Illuminate/Foundation/Console/stubs/test.stub b/src/Illuminate/Foundation/Console/stubs/test.stub index c5b50ad3cb6b..84c75cbfe98d 100644 --- a/src/Illuminate/Foundation/Console/stubs/test.stub +++ b/src/Illuminate/Foundation/Console/stubs/test.stub @@ -13,7 +13,7 @@ class {{ class }} extends TestCase * * @return void */ - public function testExample() + public function test_example() { $response = $this->get('/'); diff --git a/src/Illuminate/Foundation/Console/stubs/test.unit.stub b/src/Illuminate/Foundation/Console/stubs/test.unit.stub index 98af6529355c..b6816aa72f29 100644 --- a/src/Illuminate/Foundation/Console/stubs/test.unit.stub +++ b/src/Illuminate/Foundation/Console/stubs/test.unit.stub @@ -11,7 +11,7 @@ class {{ class }} extends TestCase * * @return void */ - public function testExample() + public function test_example() { $this->assertTrue(true); } diff --git a/src/Illuminate/Foundation/Console/stubs/view-component.stub b/src/Illuminate/Foundation/Console/stubs/view-component.stub index 5c6ecc586ec4..22eae518c1a4 100644 --- a/src/Illuminate/Foundation/Console/stubs/view-component.stub +++ b/src/Illuminate/Foundation/Console/stubs/view-component.stub @@ -19,7 +19,7 @@ class DummyClass extends Component /** * Get the view / contents that represent the component. * - * @return \Illuminate\Contracts\View\View|string + * @return \Illuminate\Contracts\View\View|\Closure|string */ public function render() { diff --git a/src/Illuminate/Foundation/Events/VendorTagPublished.php b/src/Illuminate/Foundation/Events/VendorTagPublished.php new file mode 100644 index 000000000000..084c1293fcfd --- /dev/null +++ b/src/Illuminate/Foundation/Events/VendorTagPublished.php @@ -0,0 +1,33 @@ +tag = $tag; + $this->paths = $paths; + } +} diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 924046b8b340..bedb1fca3be4 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -11,6 +11,8 @@ use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\MultipleRecordsFoundException; +use Illuminate\Database\RecordsNotFoundException; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -19,7 +21,6 @@ use Illuminate\Session\TokenMismatchException; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\View; use Illuminate\Support\Reflector; use Illuminate\Support\Traits\ReflectsClosures; use Illuminate\Support\ViewErrorBag; @@ -89,6 +90,8 @@ class Handler implements ExceptionHandlerContract HttpException::class, HttpResponseException::class, ModelNotFoundException::class, + MultipleRecordsFoundException::class, + RecordsNotFoundException::class, SuspiciousOperationException::class, TokenMismatchException::class, ValidationException::class, @@ -100,6 +103,7 @@ class Handler implements ExceptionHandlerContract * @var string[] */ protected $dontFlash = [ + 'current_password', 'password', 'password_confirmation', ]; @@ -135,6 +139,10 @@ public function register() */ public function reportable(callable $reportUsing) { + if (! $reportUsing instanceof Closure) { + $reportUsing = Closure::fromCallable($reportUsing); + } + return tap(new ReportableHandler($reportUsing), function ($callback) { $this->reportCallbacks[] = $callback; }); @@ -148,6 +156,10 @@ public function reportable(callable $reportUsing) */ public function renderable(callable $renderUsing) { + if (! $renderUsing instanceof Closure) { + $renderUsing = Closure::fromCallable($renderUsing); + } + $this->renderCallbacks[] = $renderUsing; return $this; @@ -274,6 +286,10 @@ protected function shouldntReport(Throwable $e) */ protected function exceptionContext(Throwable $e) { + if (method_exists($e, 'context')) { + return $e->context(); + } + return []; } @@ -369,6 +385,8 @@ protected function prepareException(Throwable $e) $e = new HttpException(419, $e->getMessage(), $e); } elseif ($e instanceof SuspiciousOperationException) { $e = new NotFoundHttpException('Bad hostname provided.', $e); + } elseif ($e instanceof RecordsNotFoundException) { + $e = new NotFoundHttpException('Not found.', $e); } return $e; diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 96169f3ce40a..8c2da9699600 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -58,6 +58,13 @@ class FormRequest extends Request implements ValidatesWhenResolved */ protected $errorBag = 'default'; + /** + * Indicates whether validation should stop after the first rule failure. + * + * @var bool + */ + protected $stopOnFirstFailure = false; + /** * The validator instance. * @@ -104,7 +111,7 @@ protected function createDefaultValidator(ValidationFactory $factory) return $factory->make( $this->validationData(), $this->container->call([$this, 'rules']), $this->messages(), $this->attributes() - ); + )->stopOnFirstFailure($this->stopOnFirstFailure); } /** diff --git a/src/Illuminate/Foundation/Http/Kernel.php b/src/Illuminate/Foundation/Http/Kernel.php index 656bf0f1d164..368bb5fa0471 100644 --- a/src/Illuminate/Foundation/Http/Kernel.php +++ b/src/Illuminate/Foundation/Http/Kernel.php @@ -254,7 +254,7 @@ public function hasMiddleware($middleware) } /** - * Add a new middleware to beginning of the stack if it does not already exist. + * Add a new middleware to the beginning of the stack if it does not already exist. * * @param string $middleware * @return $this @@ -445,4 +445,17 @@ public function getApplication() { return $this->app; } + + /** + * Set the Laravel application instance. + * + * @param \Illuminate\Contracts\Foundation\Application + * @return $this + */ + public function setApplication(Application $app) + { + $this->app = $app; + + return $this; + } } diff --git a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php index 99953bfd5e90..831468281fbc 100644 --- a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -42,7 +42,6 @@ public function __construct(Application $app) * @return mixed * * @throws \Symfony\Component\HttpKernel\Exception\HttpException - * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function handle($request, Closure $next) { diff --git a/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php b/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php index a61a1bd72013..fca34f837b0b 100644 --- a/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php +++ b/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php @@ -58,9 +58,11 @@ protected function cleanParameterBag(ParameterBag $bag) */ protected function cleanArray(array $data, $keyPrefix = '') { - return collect($data)->map(function ($value, $key) use ($keyPrefix) { - return $this->cleanValue($keyPrefix.$key, $value); - })->all(); + foreach ($data as $key => $value) { + $data[$key] = $this->cleanValue($keyPrefix.$key, $value); + } + + return collect($data)->all(); } /** diff --git a/src/Illuminate/Foundation/Inspiring.php b/src/Illuminate/Foundation/Inspiring.php index 6023f5635029..a7e7524e19d9 100644 --- a/src/Illuminate/Foundation/Inspiring.php +++ b/src/Illuminate/Foundation/Inspiring.php @@ -45,6 +45,11 @@ public static function quote() 'Waste no more time arguing what a good man should be, be one. - Marcus Aurelius', 'Well begun is half done. - Aristotle', 'When there is no desire, all things are at peace. - Laozi', + 'Walk as if you are kissing the Earth with your feet. - Thich Nhat Hanh', + 'Because you are alive, everything is possible. - Thich Nhat Hanh', + 'Breathing in, I calm body and mind. Breathing out, I smile. - Thich Nhat Hanh', + 'Life is available only in the present moment. - Thich Nhat Hanh', + 'The best way to take care of the future is to take care of the present moment. - Thich Nhat Hanh', ])->random(); } } diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index 9cce44c00e3f..a0dd7067b555 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -67,6 +67,7 @@ use Illuminate\Queue\Console\ForgetFailedCommand as ForgetFailedQueueCommand; use Illuminate\Queue\Console\ListenCommand as QueueListenCommand; use Illuminate\Queue\Console\ListFailedCommand as ListFailedQueueCommand; +use Illuminate\Queue\Console\PruneBatchesCommand as PruneBatchesQueueCommand; use Illuminate\Queue\Console\RestartCommand as QueueRestartCommand; use Illuminate\Queue\Console\RetryBatchCommand as QueueRetryBatchCommand; use Illuminate\Queue\Console\RetryCommand as QueueRetryCommand; @@ -107,6 +108,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'QueueFlush' => 'command.queue.flush', 'QueueForget' => 'command.queue.forget', 'QueueListen' => 'command.queue.listen', + 'QueuePruneBatches' => 'command.queue.prune-batches', 'QueueRestart' => 'command.queue.restart', 'QueueRetry' => 'command.queue.retry', 'QueueRetryBatch' => 'command.queue.retry-batch', @@ -464,7 +466,7 @@ protected function registerEventClearCommand() protected function registerEventListCommand() { $this->app->singleton('command.event.list', function () { - return new EventListCommand(); + return new EventListCommand; }); } @@ -684,6 +686,18 @@ protected function registerQueueListenCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerQueuePruneBatchesCommand() + { + $this->app->singleton('command.queue.prune-batches', function () { + return new PruneBatchesQueueCommand; + }); + } + /** * Register the command. * diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 2f39afd43d36..f5ffb33658f5 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Facades\URL; +use Illuminate\Testing\ParallelTestingServiceProvider; use Illuminate\Validation\ValidationException; class FoundationServiceProvider extends AggregateServiceProvider @@ -16,6 +17,7 @@ class FoundationServiceProvider extends AggregateServiceProvider */ protected $providers = [ FormRequestServiceProvider::class, + ParallelTestingServiceProvider::class, ]; /** diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index 0573563cf5ac..70ea3086efe9 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -119,7 +119,7 @@ public function discoverEvents() ->reduce(function ($discovered, $directory) { return array_merge_recursive( $discovered, - DiscoverEvents::within($directory, base_path()) + DiscoverEvents::within($directory, $this->eventDiscoveryBasePath()) ); }, []); } @@ -135,4 +135,14 @@ protected function discoverEventsWithin() $this->app->path('Listeners'), ]; } + + /** + * Get the base path to be used during event discovery. + * + * @return string + */ + protected function eventDiscoveryBasePath() + { + return base_path(); + } } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php index dad0f65f6464..38409d3d697f 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php @@ -30,7 +30,7 @@ trait InteractsWithConsole public $unexpectedOutput = []; /** - * All of the expected ouput tables. + * All of the expected output tables. * * @var array */ diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php index 40e3d777ffbd..0304940ff061 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php @@ -124,7 +124,7 @@ public function render($request, Throwable $e) if ($e instanceof NotFoundHttpException) { throw new NotFoundHttpException( - "{$request->method()} {$request->url()}", null, $e->getCode() + "{$request->method()} {$request->url()}", $e, $e->getCode() ); } diff --git a/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php b/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php index 7fc360e76f75..66622950c766 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php @@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Event; use Mockery; +/** + * @deprecated Will be removed in a future Laravel version. + */ trait MocksApplicationServices { /** diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabase.php b/src/Illuminate/Foundation/Testing/RefreshDatabase.php index f62fad83c559..d66fd0f94911 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabase.php @@ -79,11 +79,15 @@ protected function refreshTestDatabase() */ protected function migrateFreshUsing() { - return [ - '--drop-views' => $this->shouldDropViews(), - '--drop-types' => $this->shouldDropTypes(), - '--seed' => $this->shouldSeed(), - ]; + $seeder = $this->seeder(); + + return array_merge( + [ + '--drop-views' => $this->shouldDropViews(), + '--drop-types' => $this->shouldDropTypes(), + ], + $seeder ? ['--seeder' => $seeder] : ['--seed' => $this->shouldSeed()] + ); } /** @@ -157,4 +161,14 @@ protected function shouldSeed() { return property_exists($this, 'seed') ? $this->seed : false; } + + /** + * Determine the specific seeder class that should be used when refreshing the database. + * + * @return mixed + */ + protected function seeder() + { + return property_exists($this, 'seeder') ? $this->seeder : false; + } } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index b32202517ceb..ee19a864b591 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -6,7 +6,9 @@ use Carbon\CarbonImmutable; use Illuminate\Console\Application as Artisan; use Illuminate\Database\Eloquent\Model; +use Illuminate\Queue\Queue; use Illuminate\Support\Facades\Facade; +use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Str; use Mockery; use Mockery\Exception\InvalidCountException; @@ -81,6 +83,8 @@ protected function setUp(): void if (! $this->app) { $this->refreshApplication(); + + ParallelTesting::callSetUpTestCaseCallbacks($this); } $this->setUpTraits(); @@ -152,6 +156,8 @@ protected function tearDown(): void if ($this->app) { $this->callBeforeApplicationDestroyedCallbacks(); + ParallelTesting::callTearDownTestCaseCallbacks($this); + $this->app->flush(); $this->app = null; @@ -194,6 +200,8 @@ protected function tearDown(): void Artisan::forgetBootstrappers(); + Queue::createPayloadUsing(null); + if ($this->callbackException) { throw $this->callbackException; } diff --git a/src/Illuminate/Foundation/Testing/Wormhole.php b/src/Illuminate/Foundation/Testing/Wormhole.php index ef02e5a26f03..6258f6de2e11 100644 --- a/src/Illuminate/Foundation/Testing/Wormhole.php +++ b/src/Illuminate/Foundation/Testing/Wormhole.php @@ -2,7 +2,7 @@ namespace Illuminate\Foundation\Testing; -use Illuminate\Support\Facades\Date; +use Illuminate\Support\Carbon; class Wormhole { @@ -32,7 +32,7 @@ public function __construct($value) */ public function milliseconds($callback = null) { - Date::setTestNow(Date::now()->addMilliseconds($this->value)); + Carbon::setTestNow(Carbon::now()->addMilliseconds($this->value)); return $this->handleCallback($callback); } @@ -45,7 +45,7 @@ public function milliseconds($callback = null) */ public function seconds($callback = null) { - Date::setTestNow(Date::now()->addSeconds($this->value)); + Carbon::setTestNow(Carbon::now()->addSeconds($this->value)); return $this->handleCallback($callback); } @@ -58,7 +58,7 @@ public function seconds($callback = null) */ public function minutes($callback = null) { - Date::setTestNow(Date::now()->addMinutes($this->value)); + Carbon::setTestNow(Carbon::now()->addMinutes($this->value)); return $this->handleCallback($callback); } @@ -71,7 +71,7 @@ public function minutes($callback = null) */ public function hours($callback = null) { - Date::setTestNow(Date::now()->addHours($this->value)); + Carbon::setTestNow(Carbon::now()->addHours($this->value)); return $this->handleCallback($callback); } @@ -84,7 +84,7 @@ public function hours($callback = null) */ public function days($callback = null) { - Date::setTestNow(Date::now()->addDays($this->value)); + Carbon::setTestNow(Carbon::now()->addDays($this->value)); return $this->handleCallback($callback); } @@ -97,7 +97,20 @@ public function days($callback = null) */ public function weeks($callback = null) { - Date::setTestNow(Date::now()->addWeeks($this->value)); + Carbon::setTestNow(Carbon::now()->addWeeks($this->value)); + + return $this->handleCallback($callback); + } + + /** + * Travel forward the given number of months. + * + * @param callable|null $callback + * @return mixed + */ + public function months($callback = null) + { + Carbon::setTestNow(Carbon::now()->addMonths($this->value)); return $this->handleCallback($callback); } @@ -110,7 +123,7 @@ public function weeks($callback = null) */ public function years($callback = null) { - Date::setTestNow(Date::now()->addYears($this->value)); + Carbon::setTestNow(Carbon::now()->addYears($this->value)); return $this->handleCallback($callback); } @@ -122,9 +135,9 @@ public function years($callback = null) */ public static function back() { - Date::setTestNow(); + Carbon::setTestNow(); - return Date::now(); + return Carbon::now(); } /** @@ -137,7 +150,7 @@ protected function handleCallback($callback) { if ($callback) { return tap($callback(), function () { - Date::setTestNow(); + Carbon::setTestNow(); }); } } diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index d6af860d8915..3842915bf721 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -586,11 +586,15 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null) /** * Report an exception. * - * @param \Throwable $exception + * @param \Throwable|string $exception * @return void */ - function report(Throwable $exception) + function report($exception) { + if (is_string($exception)) { + $exception = new Exception($exception); + } + app(ExceptionHandler::class)->report($exception); } } @@ -601,7 +605,7 @@ function report(Throwable $exception) * * @param array|string|null $key * @param mixed $default - * @return \Illuminate\Http\Request|string|array + * @return \Illuminate\Http\Request|string|array|null */ function request($key = null, $default = null) { diff --git a/src/Illuminate/Hashing/ArgonHasher.php b/src/Illuminate/Hashing/ArgonHasher.php index 41109c9b0799..ea3a2f34cc00 100644 --- a/src/Illuminate/Hashing/ArgonHasher.php +++ b/src/Illuminate/Hashing/ArgonHasher.php @@ -180,7 +180,7 @@ protected function time(array $options) } /** - * Extract the threads value from the options array. + * Extract the thread's value from the options array. * * @param array $options * @return int diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 7a21d96dca31..4db3e1fa98a0 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -32,6 +32,8 @@ * @method \Illuminate\Http\Client\PendingRequest withToken(string $token, string $type = 'Bearer') * @method \Illuminate\Http\Client\PendingRequest withoutRedirecting() * @method \Illuminate\Http\Client\PendingRequest withoutVerifying() + * @method \Illuminate\Http\Client\PendingRequest dump() + * @method \Illuminate\Http\Client\PendingRequest dd() * @method \Illuminate\Http\Client\Response delete(string $url, array $data = []) * @method \Illuminate\Http\Client\Response get(string $url, array $query = []) * @method \Illuminate\Http\Client\Response head(string $url, array $query = []) @@ -225,7 +227,7 @@ public function assertSent($callback) } /** - * Assert that the given request were sent in the given order. + * Assert that the given request was sent in the given order. * * @param array $callbacks * @return void diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 836e8b39fe50..5801bc0b70f0 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use Symfony\Component\VarDumper\VarDumper; class PendingRequest { @@ -452,6 +453,40 @@ public function beforeSending($callback) }); } + /** + * Dump the request before sending. + * + * @return $this + */ + public function dump() + { + $values = func_get_args(); + + return $this->beforeSending(function (Request $request, array $options) use ($values) { + foreach (array_merge($values, [$request, $options]) as $value) { + VarDumper::dump($value); + } + }); + } + + /** + * Dump the request before sending and end the script. + * + * @return $this + */ + public function dd() + { + $values = func_get_args(); + + return $this->beforeSending(function (Request $request, array $options) use ($values) { + foreach (array_merge($values, [$request, $options]) as $value) { + VarDumper::dump($value); + } + + exit(1); + }); + } + /** * Issue a GET request to the given URL. * diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index f65a8d5ca1ba..ccbd631c7cb8 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -3,6 +3,7 @@ namespace Illuminate\Http\Client; use ArrayAccess; +use Illuminate\Support\Collection; use Illuminate\Support\Traits\Macroable; use LogicException; @@ -77,6 +78,17 @@ public function object() return json_decode($this->body(), false); } + /** + * Get the JSON decoded body of the response as a collection. + * + * @param string|null $key + * @return \Illuminate\Support\Collection + */ + public function collect($key = null) + { + return Collection::make($this->json($key)); + } + /** * Get a header from the response. * diff --git a/src/Illuminate/Http/Client/ResponseSequence.php b/src/Illuminate/Http/Client/ResponseSequence.php index 66d0ec6bbce4..0fb6fb021dd6 100644 --- a/src/Illuminate/Http/Client/ResponseSequence.php +++ b/src/Illuminate/Http/Client/ResponseSequence.php @@ -2,10 +2,13 @@ namespace Illuminate\Http\Client; +use Illuminate\Support\Traits\Macroable; use OutOfBoundsException; class ResponseSequence { + use Macroable; + /** * The responses in the sequence. * diff --git a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php index f938bf48492f..faf25d92e081 100644 --- a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php +++ b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php @@ -13,7 +13,7 @@ trait InteractsWithContentTypes */ public function isJson() { - return Str::contains($this->header('CONTENT_TYPE'), ['/json', '+json']); + return Str::contains($this->header('CONTENT_TYPE') ?? '', ['/json', '+json']); } /** diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index cf6b90cb1da0..06f143c6020f 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -142,7 +142,7 @@ public function path() { $pattern = trim($this->getPathInfo(), '/'); - return $pattern == '' ? '/' : $pattern; + return $pattern === '' ? '/' : $pattern; } /** @@ -212,7 +212,7 @@ public function routeIs(...$patterns) } /** - * Determine if the current request URL and query string matches a pattern. + * Determine if the current request URL and query string match a pattern. * * @param mixed ...$patterns * @return bool @@ -241,7 +241,7 @@ public function ajax() } /** - * Determine if the request is the result of an PJAX call. + * Determine if the request is the result of a PJAX call. * * @return bool */ @@ -251,14 +251,14 @@ public function pjax() } /** - * Determine if the request is the result of an prefetch call. + * Determine if the request is the result of a prefetch call. * * @return bool */ public function prefetch() { - return strcasecmp($this->server->get('HTTP_X_MOZ'), 'prefetch') === 0 || - strcasecmp($this->headers->get('Purpose'), 'prefetch') === 0; + return strcasecmp($this->server->get('HTTP_X_MOZ') ?? '', 'prefetch') === 0 || + strcasecmp($this->headers->get('Purpose') ?? '', 'prefetch') === 0; } /** diff --git a/src/Illuminate/Http/Resources/CollectsResources.php b/src/Illuminate/Http/Resources/CollectsResources.php index a5531f7a02ce..5c42da4225f5 100644 --- a/src/Illuminate/Http/Resources/CollectsResources.php +++ b/src/Illuminate/Http/Resources/CollectsResources.php @@ -47,7 +47,8 @@ protected function collects() } if (Str::endsWith(class_basename($this), 'Collection') && - class_exists($class = Str::replaceLast('Collection', '', get_class($this)))) { + (class_exists($class = Str::replaceLast('Collection', '', get_class($this))) || + class_exists($class = Str::replaceLast('Collection', 'Resource', get_class($this))))) { return $class; } } diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 808aa234d5b8..3f2175f4dfe6 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -69,7 +69,7 @@ public static function make(...$parameters) } /** - * Create new anonymous resource collection. + * Create a new anonymous resource collection. * * @param mixed $resource * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection diff --git a/src/Illuminate/Http/Resources/MergeValue.php b/src/Illuminate/Http/Resources/MergeValue.php index ee557e8f3b87..fb6880fb725c 100644 --- a/src/Illuminate/Http/Resources/MergeValue.php +++ b/src/Illuminate/Http/Resources/MergeValue.php @@ -15,7 +15,7 @@ class MergeValue public $data; /** - * Create new merge value instance. + * Create a new merge value instance. * * @param \Illuminate\Support\Collection|\JsonSerializable|array $data * @return void diff --git a/src/Illuminate/Http/Testing/MimeType.php b/src/Illuminate/Http/Testing/MimeType.php index aff03d4bbba6..d188a4be35e8 100644 --- a/src/Illuminate/Http/Testing/MimeType.php +++ b/src/Illuminate/Http/Testing/MimeType.php @@ -22,7 +22,7 @@ class MimeType public static function getMimeTypes() { if (self::$mime === null) { - self::$mime = new MimeTypes(); + self::$mime = new MimeTypes; } return self::$mime; diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 0e0abcd67c33..f5d0ac486e2b 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -245,11 +245,15 @@ protected function createStackDriver(array $config) return $this->channel($channel)->getHandlers(); })->all(); + $processors = collect($config['channels'])->flatMap(function ($channel) { + return $this->channel($channel)->getProcessors(); + })->all(); + if ($config['ignore_exceptions'] ?? false) { $handlers = [new WhatFailureGroupHandler($handlers)]; } - return new Monolog($this->parseChannel($config), $handlers); + return new Monolog($this->parseChannel($config), $handlers, $processors); } /** diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index a8a4a291d0c9..7437e699c5e0 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -63,7 +63,7 @@ public function __construct($app) * Get a mailer instance by name. * * @param string|null $name - * @return \Illuminate\Mail\Mailer + * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null) { @@ -327,10 +327,15 @@ protected function createMailgunTransport(array $config) */ protected function createPostmarkTransport(array $config) { + $headers = isset($config['message_stream_id']) ? [ + 'X-PM-Message-Stream' => $config['message_stream_id'], + ] : []; + return tap(new PostmarkTransport( - $config['token'] ?? $this->app['config']->get('services.postmark.token') + $config['token'] ?? $this->app['config']->get('services.postmark.token'), + $headers ), function ($transport) { - $transport->registerPlugin(new ThrowExceptionOnFailurePlugin()); + $transport->registerPlugin(new ThrowExceptionOnFailurePlugin); }); } @@ -467,6 +472,41 @@ public function extend($driver, Closure $callback) return $this; } + /** + * Get the application instance used by the manager. + * + * @return \Illuminate\Contracts\Foundation\Application + */ + public function getApplication() + { + return $this->app; + } + + /** + * Set the application instance used by the manager. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + return $this; + } + + /** + * Forget all of the resolved mailer instances. + * + * @return $this + */ + public function forgetMailers() + { + $this->mailers = []; + + return $this; + } + /** * Dynamically call the default driver instance. * diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 1fe0b7ea3d20..903bd5f5f41b 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -170,7 +170,7 @@ class Mailable implements MailableContract, Renderable */ public function send($mailer) { - return $this->withLocale($this->locale, function () use ($mailer) { + $this->withLocale($this->locale, function () use ($mailer) { Container::getInstance()->call([$this, 'build']); $mailer = $mailer instanceof MailFactory diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 84e14b4a7b98..128f211f7651 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -197,7 +197,7 @@ public function bcc($users) */ public function html($html, $callback) { - return $this->send(['html' => new HtmlString($html)], [], $callback); + $this->send(['html' => new HtmlString($html)], [], $callback); } /** @@ -209,7 +209,7 @@ public function html($html, $callback) */ public function raw($text, $callback) { - return $this->send(['raw' => $text], [], $callback); + $this->send(['raw' => $text], [], $callback); } /** @@ -222,7 +222,7 @@ public function raw($text, $callback) */ public function plain($view, array $data, $callback) { - return $this->send(['text' => $view], $data, $callback); + $this->send(['text' => $view], $data, $callback); } /** @@ -352,7 +352,7 @@ protected function parseView($view) protected function addContent($message, $view, $plain, $raw, $data) { if (isset($view)) { - $message->setBody($this->renderView($view, $data), 'text/html'); + $message->setBody($this->renderView($view, $data) ?: ' ', 'text/html'); } if (isset($plain)) { diff --git a/src/Illuminate/Mail/Markdown.php b/src/Illuminate/Mail/Markdown.php index a506f837f59f..9a1706d383b1 100644 --- a/src/Illuminate/Mail/Markdown.php +++ b/src/Illuminate/Mail/Markdown.php @@ -63,8 +63,8 @@ public function render($view, array $data = [], $inliner = null) 'mail', $this->htmlComponentPaths() )->make($view, $data)->render(); - if ($this->view->exists($this->theme)) { - $theme = $this->theme; + if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) { + $theme = $customTheme; } else { $theme = Str::contains($this->theme, '::') ? $this->theme diff --git a/src/Illuminate/Mail/Message.php b/src/Illuminate/Mail/Message.php index d701fba9fb39..cab6c026d9fe 100755 --- a/src/Illuminate/Mail/Message.php +++ b/src/Illuminate/Mail/Message.php @@ -137,7 +137,7 @@ public function bcc($address, $name = null, $override = false) } /** - * Add a reply to address to the message. + * Add a "reply to" address to the message. * * @param string|array $address * @param string|null $name diff --git a/src/Illuminate/Mail/PendingMail.php b/src/Illuminate/Mail/PendingMail.php index 43c30961f90b..10d76cb6aa9b 100644 --- a/src/Illuminate/Mail/PendingMail.php +++ b/src/Illuminate/Mail/PendingMail.php @@ -114,11 +114,11 @@ public function bcc($users) * Send a new mailable message instance. * * @param \Illuminate\Contracts\Mail\Mailable $mailable - * @return mixed + * @return void */ public function send(MailableContract $mailable) { - return $this->mailer->send($this->fill($mailable)); + $this->mailer->send($this->fill($mailable)); } /** diff --git a/src/Illuminate/Mail/SendQueuedMailable.php b/src/Illuminate/Mail/SendQueuedMailable.php index a4567dadcb4e..1009789b4bf0 100644 --- a/src/Illuminate/Mail/SendQueuedMailable.php +++ b/src/Illuminate/Mail/SendQueuedMailable.php @@ -5,6 +5,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Mail\Factory as MailFactory; use Illuminate\Contracts\Mail\Mailable as MailableContract; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; class SendQueuedMailable { @@ -31,6 +32,13 @@ class SendQueuedMailable */ public $timeout; + /** + * Indicates if the job should be encrypted. + * + * @var bool + */ + public $shouldBeEncrypted = false; + /** * Create a new job instance. * @@ -43,6 +51,7 @@ public function __construct(MailableContract $mailable) $this->tries = property_exists($mailable, 'tries') ? $mailable->tries : null; $this->timeout = property_exists($mailable, 'timeout') ? $mailable->timeout : null; $this->afterCommit = property_exists($mailable, 'afterCommit') ? $mailable->afterCommit : null; + $this->shouldBeEncrypted = $mailable instanceof ShouldBeEncrypted; } /** @@ -80,7 +89,7 @@ public function failed($e) } /** - * Get number of seconds before a released mailable will be available. + * Get the number of seconds before a released mailable will be available. * * @return mixed */ diff --git a/src/Illuminate/Mail/resources/views/html/layout.blade.php b/src/Illuminate/Mail/resources/views/html/layout.blade.php index 684e0f7d05e6..21d349b39ea7 100644 --- a/src/Illuminate/Mail/resources/views/html/layout.blade.php +++ b/src/Illuminate/Mail/resources/views/html/layout.blade.php @@ -5,8 +5,6 @@ - - + + diff --git a/src/Illuminate/Mail/resources/views/html/themes/default.css b/src/Illuminate/Mail/resources/views/html/themes/default.css index 350fb838fa6b..2483b11685a3 100644 --- a/src/Illuminate/Mail/resources/views/html/themes/default.css +++ b/src/Illuminate/Mail/resources/views/html/themes/default.css @@ -113,6 +113,7 @@ img { .logo { height: 75px; + max-height: 75px; width: 75px; } diff --git a/src/Illuminate/Notifications/ChannelManager.php b/src/Illuminate/Notifications/ChannelManager.php index d2344ab68acc..8eb9c251024d 100644 --- a/src/Illuminate/Notifications/ChannelManager.php +++ b/src/Illuminate/Notifications/ChannelManager.php @@ -34,7 +34,7 @@ class ChannelManager extends Manager implements DispatcherContract, FactoryContr */ public function send($notifiables, $notification) { - return (new NotificationSender( + (new NotificationSender( $this, $this->container->make(Bus::class), $this->container->make(Dispatcher::class), $this->locale) )->send($notifiables, $notification); } @@ -49,7 +49,7 @@ public function send($notifiables, $notification) */ public function sendNow($notifiables, $notification, array $channels = null) { - return (new NotificationSender( + (new NotificationSender( $this, $this->container->make(Bus::class), $this->container->make(Dispatcher::class), $this->locale) )->sendNow($notifiables, $notification, $channels); } diff --git a/src/Illuminate/Notifications/Channels/BroadcastChannel.php b/src/Illuminate/Notifications/Channels/BroadcastChannel.php index d281b9b13831..1389f49c6ac8 100644 --- a/src/Illuminate/Notifications/Channels/BroadcastChannel.php +++ b/src/Illuminate/Notifications/Channels/BroadcastChannel.php @@ -18,7 +18,7 @@ class BroadcastChannel protected $events; /** - * Create a new database channel. + * Create a new broadcast channel. * * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void diff --git a/src/Illuminate/Notifications/NotificationSender.php b/src/Illuminate/Notifications/NotificationSender.php index 39be0e598796..aff36c7a5b0f 100644 --- a/src/Illuminate/Notifications/NotificationSender.php +++ b/src/Illuminate/Notifications/NotificationSender.php @@ -76,7 +76,7 @@ public function send($notifiables, $notification) return $this->queueNotification($notifiables, $notification); } - return $this->sendNow($notifiables, $notification); + $this->sendNow($notifiables, $notification); } /** diff --git a/src/Illuminate/Notifications/SendQueuedNotifications.php b/src/Illuminate/Notifications/SendQueuedNotifications.php index 2f983023f051..d83c8906e366 100644 --- a/src/Illuminate/Notifications/SendQueuedNotifications.php +++ b/src/Illuminate/Notifications/SendQueuedNotifications.php @@ -3,6 +3,7 @@ namespace Illuminate\Notifications; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; @@ -49,6 +50,13 @@ class SendQueuedNotifications implements ShouldQueue */ public $timeout; + /** + * Indicates if the job should be encrypted. + * + * @var bool + */ + public $shouldBeEncrypted = false; + /** * Create a new job instance. * @@ -65,6 +73,7 @@ public function __construct($notifiables, $notification, array $channels = null) $this->tries = property_exists($notification, 'tries') ? $notification->tries : null; $this->timeout = property_exists($notification, 'timeout') ? $notification->timeout : null; $this->afterCommit = property_exists($notification, 'afterCommit') ? $notification->afterCommit : null; + $this->shouldBeEncrypted = $notification instanceof ShouldBeEncrypted; } /** @@ -119,7 +128,7 @@ public function failed($e) } /** - * Get number of seconds before a released notification will be available. + * Get the number of seconds before a released notification will be available. * * @return mixed */ diff --git a/src/Illuminate/Pagination/AbstractPaginator.php b/src/Illuminate/Pagination/AbstractPaginator.php index 2894d1f15ffa..763091067057 100644 --- a/src/Illuminate/Pagination/AbstractPaginator.php +++ b/src/Illuminate/Pagination/AbstractPaginator.php @@ -494,7 +494,7 @@ public static function currentPathResolver(Closure $resolver) public static function resolveCurrentPage($pageName = 'page', $default = 1) { if (isset(static::$currentPageResolver)) { - return call_user_func(static::$currentPageResolver, $pageName); + return (int) call_user_func(static::$currentPageResolver, $pageName); } return $default; @@ -739,7 +739,7 @@ public function __call($method, $parameters) } /** - * Render the contents of the paginator when casting to string. + * Render the contents of the paginator when casting to a string. * * @return string */ diff --git a/src/Illuminate/Pagination/LengthAwarePaginator.php b/src/Illuminate/Pagination/LengthAwarePaginator.php index 0260b974bc4b..3e5adad0a316 100644 --- a/src/Illuminate/Pagination/LengthAwarePaginator.php +++ b/src/Illuminate/Pagination/LengthAwarePaginator.php @@ -109,7 +109,7 @@ protected function linkCollection() return collect($item)->map(function ($url, $page) { return [ 'url' => $url, - 'label' => $page, + 'label' => (string) $page, 'active' => $this->currentPage() === $page, ]; }); diff --git a/src/Illuminate/Pagination/PaginationServiceProvider.php b/src/Illuminate/Pagination/PaginationServiceProvider.php index 6510f2f261fd..e94cebd6caf7 100755 --- a/src/Illuminate/Pagination/PaginationServiceProvider.php +++ b/src/Illuminate/Pagination/PaginationServiceProvider.php @@ -29,26 +29,6 @@ public function boot() */ public function register() { - Paginator::viewFactoryResolver(function () { - return $this->app['view']; - }); - - Paginator::currentPathResolver(function () { - return $this->app['request']->url(); - }); - - Paginator::currentPageResolver(function ($pageName = 'page') { - $page = $this->app['request']->input($pageName); - - if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { - return (int) $page; - } - - return 1; - }); - - Paginator::queryStringResolver(function () { - return $this->app['request']->query(); - }); + PaginationState::resolveUsing($this->app); } } diff --git a/src/Illuminate/Pagination/PaginationState.php b/src/Illuminate/Pagination/PaginationState.php new file mode 100644 index 000000000000..f71ea13bde94 --- /dev/null +++ b/src/Illuminate/Pagination/PaginationState.php @@ -0,0 +1,37 @@ +url(); + }); + + Paginator::currentPageResolver(function ($pageName = 'page') use ($app) { + $page = $app['request']->input($pageName); + + if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { + return (int) $page; + } + + return 1; + }); + + Paginator::queryStringResolver(function () use ($app) { + return $app['request']->query(); + }); + } +} diff --git a/src/Illuminate/Pipeline/Hub.php b/src/Illuminate/Pipeline/Hub.php index 87331a57b2c6..91e9b3f306b8 100644 --- a/src/Illuminate/Pipeline/Hub.php +++ b/src/Illuminate/Pipeline/Hub.php @@ -71,4 +71,27 @@ public function pipe($object, $pipeline = null) $this->pipelines[$pipeline], new Pipeline($this->container), $object ); } + + /** + * Get the container instance used by the hub. + * + * @return \Illuminate\Contracts\Container\Container + */ + public function getContainer() + { + return $this->container; + } + + /** + * Set the container instance used by the hub. + * + * @param \Illuminate\Contracts\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + + return $this; + } } diff --git a/src/Illuminate/Queue/CallQueuedClosure.php b/src/Illuminate/Queue/CallQueuedClosure.php index f101b6f0b996..28e1f35b268a 100644 --- a/src/Illuminate/Queue/CallQueuedClosure.php +++ b/src/Illuminate/Queue/CallQueuedClosure.php @@ -3,7 +3,6 @@ namespace Illuminate\Queue; use Closure; -use Exception; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Container\Container; @@ -87,10 +86,10 @@ public function onFailure($callback) /** * Handle a job failure. * - * @param \Exception $e + * @param \Throwable $e * @return void */ - public function failed(Exception $e) + public function failed($e) { foreach ($this->failureCallbacks as $callback) { call_user_func($callback instanceof SerializableClosure ? $callback->getClosure() : $callback, $e); diff --git a/src/Illuminate/Queue/Console/PruneBatchesCommand.php b/src/Illuminate/Queue/Console/PruneBatchesCommand.php new file mode 100644 index 000000000000..cc25bc53cfb9 --- /dev/null +++ b/src/Illuminate/Queue/Console/PruneBatchesCommand.php @@ -0,0 +1,43 @@ +laravel[BatchRepository::class]; + + if ($repository instanceof PrunableBatchRepository) { + $count = $repository->prune(Carbon::now()->subHours($this->option('hours'))); + } + + $this->info("{$count} entries deleted!"); + } +} diff --git a/src/Illuminate/Queue/Console/RetryCommand.php b/src/Illuminate/Queue/Console/RetryCommand.php index e9120a976962..2f651b60d098 100644 --- a/src/Illuminate/Queue/Console/RetryCommand.php +++ b/src/Illuminate/Queue/Console/RetryCommand.php @@ -2,8 +2,12 @@ namespace Illuminate\Queue\Console; +use DateTimeInterface; use Illuminate\Console\Command; +use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Support\Arr; +use Illuminate\Support\Str; +use RuntimeException; class RetryCommand extends Command { @@ -93,14 +97,14 @@ protected function getJobIdsByRanges(array $ranges) protected function retryJob($job) { $this->laravel['queue']->connection($job->connection)->pushRaw( - $this->resetAttempts($job->payload), $job->queue + $this->refreshRetryUntil($this->resetAttempts($job->payload)), $job->queue ); } /** * Reset the payload attempts. * - * Applicable to Redis jobs which store attempts in their payload. + * Applicable to Redis and other jobs which store attempts in their payload. * * @param string $payload * @return string @@ -115,4 +119,39 @@ protected function resetAttempts($payload) return json_encode($payload); } + + /** + * Refresh the "retry until" timestamp for the job. + * + * @param string $payload + * @return string + */ + protected function refreshRetryUntil($payload) + { + $payload = json_decode($payload, true); + + if (! isset($payload['data']['command'])) { + return json_encode($payload); + } + + if (Str::startsWith($payload['data']['command'], 'O:')) { + $instance = unserialize($payload['data']['command']); + } elseif ($this->laravel->bound(Encrypter::class)) { + $instance = unserialize($this->laravel->make(Encrypter::class)->decrypt($payload['data']['command'])); + } + + if (! isset($instance)) { + throw new RuntimeException('Unable to extract job payload.'); + } + + if (is_object($instance) && method_exists($instance, 'retryUntil')) { + $retryUntil = $instance->retryUntil(); + + $payload['retryUntil'] = $retryUntil instanceof DateTimeInterface + ? $retryUntil->getTimestamp() + : $retryUntil; + } + + return json_encode($payload); + } } diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index c8c815eefe24..da9176be4063 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -33,6 +33,7 @@ class WorkCommand extends Command {--force : Force the worker to run even in maintenance mode} {--memory=128 : The memory limit in megabytes} {--sleep=3 : Number of seconds to sleep when no job is available} + {--rest=0 : Number of seconds to rest between jobs} {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed}'; @@ -124,13 +125,9 @@ protected function runWorker($connection, $queue) */ protected function gatherWorkerOptions() { - $backoff = $this->hasOption('backoff') - ? $this->option('backoff') - : $this->option('delay'); - return new WorkerOptions( $this->option('name'), - $backoff, + max($this->option('backoff'), $this->option('delay')), $this->option('memory'), $this->option('timeout'), $this->option('sleep'), @@ -138,7 +135,8 @@ protected function gatherWorkerOptions() $this->option('force'), $this->option('stop-when-empty'), $this->option('max-jobs'), - $this->option('max-time') + $this->option('max-time'), + $this->option('rest') ); } diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index 961b4e460b04..1ca050f48e50 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -47,14 +47,20 @@ class DatabaseQueue extends Queue implements QueueContract, ClearableQueue * @param string $table * @param string $default * @param int $retryAfter + * @param bool $dispatchAfterCommit * @return void */ - public function __construct(Connection $database, $table, $default = 'default', $retryAfter = 60) + public function __construct(Connection $database, + $table, + $default = 'default', + $retryAfter = 60, + $dispatchAfterCommit = false) { $this->table = $table; $this->default = $default; $this->database = $database; $this->retryAfter = $retryAfter; + $this->dispatchAfterCommit = $dispatchAfterCommit; } /** @@ -247,8 +253,8 @@ protected function getLockForPopping() $databaseEngine = $this->database->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME); $databaseVersion = $this->database->getConfig('version') ?? $this->database->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); - if ($databaseEngine == 'mysql' && ! strpos($databaseVersion, 'MariaDB') && version_compare($databaseVersion, '8.0.1', '>=') || - $databaseEngine == 'pgsql' && version_compare($databaseVersion, '9.5', '>=')) { + if ($databaseEngine === 'mysql' && ! strpos($databaseVersion, 'MariaDB') && version_compare($databaseVersion, '8.0.1', '>=') || + $databaseEngine === 'pgsql' && version_compare($databaseVersion, '9.5', '>=')) { return 'FOR UPDATE SKIP LOCKED'; } diff --git a/src/Illuminate/Queue/Events/JobQueued.php b/src/Illuminate/Queue/Events/JobQueued.php new file mode 100644 index 000000000000..c91d14095963 --- /dev/null +++ b/src/Illuminate/Queue/Events/JobQueued.php @@ -0,0 +1,42 @@ +connectionName = $connectionName; + $this->id = $id; + $this->job = $job; + } +} diff --git a/src/Illuminate/Queue/Middleware/RateLimited.php b/src/Illuminate/Queue/Middleware/RateLimited.php index efb0f60841f2..3dd1b435bdc9 100644 --- a/src/Illuminate/Queue/Middleware/RateLimited.php +++ b/src/Illuminate/Queue/Middleware/RateLimited.php @@ -99,7 +99,7 @@ protected function handleJob($job, $next, array $limits) } /** - * Do not release the job back to the queue if limit is exceeded. + * Do not release the job back to the queue if the limit is exceeded. * * @return $this */ @@ -120,4 +120,27 @@ protected function getTimeUntilNextRetry($key) { return $this->limiter->availableIn($key) + 3; } + + /** + * Prepare the object for serialization. + * + * @return array + */ + public function __sleep() + { + return [ + 'limiterName', + 'shouldRelease', + ]; + } + + /** + * Prepare the object after unserialization. + * + * @return void + */ + public function __wakeup() + { + $this->limiter = Container::getInstance()->make(RateLimiter::class); + } } diff --git a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php index 7000e9d55307..e919786f27c6 100644 --- a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php +++ b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php @@ -88,4 +88,16 @@ protected function getTimeUntilNextRetry($key) { return ($this->decaysAt[$key] - $this->currentTime()) + 3; } + + /** + * Prepare the object after unserialization. + * + * @return void + */ + public function __wakeup() + { + parent::__wakeup(); + + $this->redis = Container::getInstance()->make(Redis::class); + } } diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php new file mode 100644 index 000000000000..d289989c807b --- /dev/null +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -0,0 +1,202 @@ +maxAttempts = $maxAttempts; + $this->decayMinutes = $decayMinutes; + } + + /** + * Process the job. + * + * @param mixed $job + * @param callable $next + * @return mixed + */ + public function handle($job, $next) + { + $this->limiter = Container::getInstance()->make(RateLimiter::class); + + if ($this->limiter->tooManyAttempts($jobKey = $this->getKey($job), $this->maxAttempts)) { + return $job->release($this->getTimeUntilNextRetry($jobKey)); + } + + try { + $next($job); + + $this->limiter->clear($jobKey); + } catch (Throwable $throwable) { + if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable)) { + throw $throwable; + } + + $this->limiter->hit($jobKey, $this->decayMinutes * 60); + + return $job->release($this->retryAfterMinutes * 60); + } + } + + /** + * Specify a callback that should determine if rate limiting behavior should apply. + * + * @param callable $callback + * @return $this + */ + public function when(callable $callback) + { + $this->whenCallback = $callback; + + return $this; + } + + /** + * Set the prefix of the rate limiter key. + * + * @param string $prefix + * @return $this + */ + public function withPrefix(string $prefix) + { + $this->prefix = $prefix; + + return $this; + } + + /** + * Specify the number of minutes a job should be delayed when it is released (before it has reached its max exceptions). + * + * @param int $backoff + * @return $this + */ + public function backoff($backoff) + { + $this->retryAfterMinutes = $backoff; + + return $this; + } + + /** + * Get the cache key associated for the rate limiter. + * + * @param mixed $job + * @return string + */ + protected function getKey($job) + { + if ($this->key) { + return $this->prefix.$this->key; + } elseif ($this->byJob) { + return $this->prefix.$job->job->uuid(); + } + + return $this->prefix.md5(get_class($job)); + } + + /** + * Set the value that the rate limiter should be keyed by. + * + * @param string $key + * @return $this + */ + public function by($key) + { + $this->key = $key; + + return $this; + } + + /** + * Indicate that the throttle key should use the job's UUID. + * + * @return $this + */ + public function byJob() + { + $this->byJob = true; + + return $this; + } + + /** + * Get the number of seconds that should elapse before the job is retried. + * + * @param string $key + * @return int + */ + protected function getTimeUntilNextRetry($key) + { + return $this->limiter->availableIn($key) + 3; + } +} diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php new file mode 100644 index 000000000000..38790e353e2d --- /dev/null +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -0,0 +1,62 @@ +redis = Container::getInstance()->make(Redis::class); + + $this->limiter = new DurationLimiter( + $this->redis, $this->getKey($job), $this->maxAttempts, $this->decayMinutes * 60 + ); + + if ($this->limiter->tooManyAttempts()) { + return $job->release($this->limiter->decaysAt - $this->currentTime()); + } + + try { + $next($job); + + $this->limiter->clear(); + } catch (Throwable $throwable) { + if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable)) { + throw $throwable; + } + + $this->limiter->acquire(); + + return $job->release($this->retryAfterMinutes * 60); + } + } +} diff --git a/src/Illuminate/Queue/Queue.php b/src/Illuminate/Queue/Queue.php index b22ed51cd446..52bd32e924e8 100755 --- a/src/Illuminate/Queue/Queue.php +++ b/src/Illuminate/Queue/Queue.php @@ -7,6 +7,7 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Queue\ShouldBeEncrypted; +use Illuminate\Queue\Events\JobQueued; use Illuminate\Support\Arr; use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; @@ -151,7 +152,7 @@ protected function createObjectPayload($job, $queue) ], ]); - $command = $job instanceof ShouldBeEncrypted && $this->container->bound(Encrypter::class) + $command = $this->jobShouldBeEncrypted($job) && $this->container->bound(Encrypter::class) ? $this->container[Encrypter::class]->encrypt(serialize(clone $job)) : serialize(clone $job); @@ -212,6 +213,21 @@ public function getJobExpiration($job) ? $expiration->getTimestamp() : $expiration; } + /** + * Determine if the job should be encrypted. + * + * @param object $job + * @return bool + */ + protected function jobShouldBeEncrypted($job) + { + if ($job instanceof ShouldBeEncrypted) { + return true; + } + + return isset($job->shouldBeEncrypted) && $job->shouldBeEncrypted; + } + /** * Create a typical, string based queue payload array. * @@ -237,7 +253,7 @@ protected function createStringPayload($job, $queue, $data) /** * Register a callback to be executed when creating job payloads. * - * @param callable $callback + * @param callable|null $callback * @return void */ public static function createPayloadUsing($callback) @@ -284,13 +300,17 @@ protected function enqueueUsing($job, $payload, $queue, $delay, $callback) if ($this->shouldDispatchAfterCommit($job) && $this->container->bound('db.transactions')) { return $this->container->make('db.transactions')->addCallback( - function () use ($payload, $queue, $delay, $callback) { - return $callback($payload, $queue, $delay); + function () use ($payload, $queue, $delay, $callback, $job) { + return tap($callback($payload, $queue, $delay), function ($jobId) use ($job) { + $this->raiseJobQueuedEvent($jobId, $job); + }); } ); } - return $callback($payload, $queue, $delay); + return tap($callback($payload, $queue, $delay), function ($jobId) use ($job) { + $this->raiseJobQueuedEvent($jobId, $job); + }); } /** @@ -312,6 +332,20 @@ protected function shouldDispatchAfterCommit($job) return false; } + /** + * Raise the job queued event. + * + * @param string|int|null $jobId + * @param \Closure|string|object $job + * @return void + */ + protected function raiseJobQueuedEvent($jobId, $job) + { + if ($this->container->bound('events')) { + $this->container['events']->dispatch(new JobQueued($this->connectionName, $jobId, $job)); + } + } + /** * Get the connection name for the queue. * @@ -335,6 +369,16 @@ public function setConnectionName($name) return $this; } + /** + * Get the container instance being used by the connection. + * + * @return \Illuminate\Container\Container + */ + public function getContainer() + { + return $this->container; + } + /** * Set the IoC container instance. * diff --git a/src/Illuminate/Queue/QueueManager.php b/src/Illuminate/Queue/QueueManager.php index d19a16a750ae..624836637c02 100755 --- a/src/Illuminate/Queue/QueueManager.php +++ b/src/Illuminate/Queue/QueueManager.php @@ -246,6 +246,33 @@ public function getName($connection = null) return $connection ?: $this->getDefaultDriver(); } + /** + * Get the application instance used by the manager. + * + * @return \Illuminate\Contracts\Foundation\Application + */ + public function getApplication() + { + return $this->app; + } + + /** + * Set the application instance used by the manager. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + foreach ($this->connections as $connection) { + $connection->setContainer($app); + } + + return $this; + } + /** * Dynamically pass calls to the default connection. * diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index 3aca40c9c388..79efc0581f24 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -209,11 +209,7 @@ public function pop($queue = null) { $this->migrate($prefixed = $this->getQueue($queue)); - if (empty($nextJob = $this->retrieveNextJob($prefixed))) { - return; - } - - [$job, $reserved] = $nextJob; + [$job, $reserved] = $this->retrieveNextJob($prefixed); if ($reserved) { return new RedisJob( diff --git a/src/Illuminate/Queue/SerializesModels.php b/src/Illuminate/Queue/SerializesModels.php index 52c0f405d831..d60479d20708 100644 --- a/src/Illuminate/Queue/SerializesModels.php +++ b/src/Illuminate/Queue/SerializesModels.php @@ -91,7 +91,7 @@ public function __serialize() * Restore the model after serialization. * * @param array $values - * @return array + * @return void */ public function __unserialize(array $values) { @@ -122,8 +122,6 @@ public function __unserialize(array $values) $this, $this->getRestoredPropertyValue($values[$name]) ); } - - return $values; } /** diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index f2ba7b1a01ad..4229fe701691 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -156,6 +156,10 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $jobsProcessed++; $this->runJob($job, $connectionName, $options); + + if ($options->rest > 0) { + $this->sleep($options->rest); + } } else { $this->sleep($options->sleep); } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 0680f545096c..7b8d8dfeea3b 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -39,6 +39,13 @@ class WorkerOptions */ public $sleep; + /** + * The number of seconds to rest between jobs. + * + * @var int + */ + public $rest; + /** * The maximum amount of times a job may be attempted. * @@ -54,7 +61,7 @@ class WorkerOptions public $force; /** - * Indicates if the worker should stop when queue is empty. + * Indicates if the worker should stop when the queue is empty. * * @var bool */ @@ -87,14 +94,16 @@ class WorkerOptions * @param bool $stopWhenEmpty * @param int $maxJobs * @param int $maxTime + * @param int $rest * @return void */ public function __construct($name = 'default', $backoff = 0, $memory = 128, $timeout = 60, $sleep = 3, $maxTries = 1, - $force = false, $stopWhenEmpty = false, $maxJobs = 0, $maxTime = 0) + $force = false, $stopWhenEmpty = false, $maxJobs = 0, $maxTime = 0, $rest = 0) { $this->name = $name; $this->backoff = $backoff; $this->sleep = $sleep; + $this->rest = $rest; $this->force = $force; $this->memory = $memory; $this->timeout = $timeout; diff --git a/src/Illuminate/Redis/Connections/PhpRedisConnection.php b/src/Illuminate/Redis/Connections/PhpRedisConnection.php index 7eb11629a58b..86e239e6f118 100644 --- a/src/Illuminate/Redis/Connections/PhpRedisConnection.php +++ b/src/Illuminate/Redis/Connections/PhpRedisConnection.php @@ -71,7 +71,7 @@ public function mget(array $keys) } /** - * Set the string value in argument as value of the key. + * Set the string value in the argument as the value of the key. * * @param string $key * @param mixed $value @@ -198,7 +198,7 @@ public function brpop(...$arguments) */ public function spop($key, $count = 1) { - return $this->command('spop', [$key, $count]); + return $this->command('spop', func_get_args()); } /** @@ -501,14 +501,8 @@ public function flushdb() return $this->command('flushdb'); } - foreach ($this->client->_masters() as [$host, $port]) { - $redis = tap(new Redis)->connect($host, $port); - - if (isset($this->config['password']) && ! empty($this->config['password'])) { - $redis->auth($this->config['password']); - } - - $redis->flushDb(); + foreach ($this->client->_masters() as $master) { + $this->client->flushDb($master); } } @@ -556,7 +550,7 @@ public function disconnect() } /** - * Apply prefix to the given key if necessary. + * Apply a prefix to the given key if necessary. * * @param string $key * @return string diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 090247feba18..37a980a1d779 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -50,7 +50,7 @@ public function connectToCluster(array $config, array $clusterOptions, array $op } /** - * Build a single cluster seed string from array. + * Build a single cluster seed string from an array. * * @param array $server * @return string diff --git a/src/Illuminate/Redis/Limiters/ConcurrencyLimiterBuilder.php b/src/Illuminate/Redis/Limiters/ConcurrencyLimiterBuilder.php index 2ba7c91602d2..e66259f59b6e 100644 --- a/src/Illuminate/Redis/Limiters/ConcurrencyLimiterBuilder.php +++ b/src/Illuminate/Redis/Limiters/ConcurrencyLimiterBuilder.php @@ -58,7 +58,7 @@ public function __construct($connection, $name) } /** - * Set the maximum number of locks that can obtained per time window. + * Set the maximum number of locks that can be obtained per time window. * * @param int $maxLocks * @return $this diff --git a/src/Illuminate/Redis/Limiters/DurationLimiter.php b/src/Illuminate/Redis/Limiters/DurationLimiter.php index 9aa594fb41f4..56dbba505435 100644 --- a/src/Illuminate/Redis/Limiters/DurationLimiter.php +++ b/src/Illuminate/Redis/Limiters/DurationLimiter.php @@ -111,6 +111,30 @@ public function acquire() return (bool) $results[0]; } + /** + * Determine if the key has been "accessed" too many times. + * + * @return bool + */ + public function tooManyAttempts() + { + [$this->decaysAt, $this->remaining] = $this->redis->eval( + $this->tooManyAttemptsLuaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks + ); + + return $this->remaining <= 0; + } + + /** + * Clear the limiter. + * + * @return void + */ + public function clear() + { + $this->redis->del($this->name); + } + /** * Get the Lua script for acquiring a lock. * @@ -143,6 +167,36 @@ protected function luaScript() end return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1} +LUA; + } + + /** + * Get the Lua script to determine if the key has been "accessed" too many times. + * + * KEYS[1] - The limiter name + * ARGV[1] - Current time in microseconds + * ARGV[2] - Current time in seconds + * ARGV[3] - Duration of the bucket + * ARGV[4] - Allowed number of tasks + * + * @return string + */ + protected function tooManyAttemptsLuaScript() + { + return <<<'LUA' + +if redis.call('EXISTS', KEYS[1]) == 0 then + return {0, ARGV[2] + ARGV[3]} +end + +if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then + return { + redis.call('HGET', KEYS[1], 'end'), + ARGV[4] - redis.call('HGET', KEYS[1], 'count') + } +end + +return {0, ARGV[2] + ARGV[3]} LUA; } } diff --git a/src/Illuminate/Redis/Limiters/DurationLimiterBuilder.php b/src/Illuminate/Redis/Limiters/DurationLimiterBuilder.php index ee378fcc7ff8..c32cb50f7213 100644 --- a/src/Illuminate/Redis/Limiters/DurationLimiterBuilder.php +++ b/src/Illuminate/Redis/Limiters/DurationLimiterBuilder.php @@ -24,7 +24,7 @@ class DurationLimiterBuilder public $name; /** - * The maximum number of locks that can obtained per time window. + * The maximum number of locks that can be obtained per time window. * * @var int */ @@ -58,7 +58,7 @@ public function __construct($connection, $name) } /** - * Set the maximum number of locks that can obtained per time window. + * Set the maximum number of locks that can be obtained per time window. * * @param int $maxLocks * @return $this diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index 3dd4ec73e34f..3d01818da2ae 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -192,7 +192,7 @@ protected function parseConnectionConfiguration($config) } return array_filter($parsed, function ($key) { - return ! in_array($key, ['driver', 'username'], true); + return ! in_array($key, ['driver'], true); }, ARRAY_FILTER_USE_KEY); } diff --git a/src/Illuminate/Routing/CompiledRouteCollection.php b/src/Illuminate/Routing/CompiledRouteCollection.php index 099156cc4b81..45a186e484c0 100644 --- a/src/Illuminate/Routing/CompiledRouteCollection.php +++ b/src/Illuminate/Routing/CompiledRouteCollection.php @@ -252,6 +252,10 @@ public function getRoutesByMethod() }) ->map(function (Collection $routes) { return $routes->mapWithKeys(function (Route $route) { + if ($domain = $route->getDomain()) { + return [$domain.'/'.$route->uri => $route]; + } + return [$route->uri => $route]; })->all(); }) @@ -293,7 +297,7 @@ protected function newRoute(array $attributes) ), '/'); } - return $this->router->newRoute($attributes['methods'], $baseUri == '' ? '/' : $baseUri, $attributes['action']) + return $this->router->newRoute($attributes['methods'], $baseUri === '' ? '/' : $baseUri, $attributes['action']) ->setFallback($attributes['fallback']) ->setDefaults($attributes['defaults']) ->setWheres($attributes['wheres']) diff --git a/src/Illuminate/Routing/Console/ControllerMakeCommand.php b/src/Illuminate/Routing/Console/ControllerMakeCommand.php index 6c78e4a959e5..0bd4b6214da0 100755 --- a/src/Illuminate/Routing/Console/ControllerMakeCommand.php +++ b/src/Illuminate/Routing/Console/ControllerMakeCommand.php @@ -86,7 +86,7 @@ protected function getDefaultNamespace($rootNamespace) /** * Build the class with the given name. * - * Remove the base controller import if we are already in base namespace. + * Remove the base controller import if we are already in the base namespace. * * @param string $name * @return string diff --git a/src/Illuminate/Routing/Exceptions/UrlGenerationException.php b/src/Illuminate/Routing/Exceptions/UrlGenerationException.php index 1853b2aff0fc..eadda8010c0f 100644 --- a/src/Illuminate/Routing/Exceptions/UrlGenerationException.php +++ b/src/Illuminate/Routing/Exceptions/UrlGenerationException.php @@ -3,6 +3,8 @@ namespace Illuminate\Routing\Exceptions; use Exception; +use Illuminate\Routing\Route; +use Illuminate\Support\Str; class UrlGenerationException extends Exception { @@ -10,10 +12,26 @@ class UrlGenerationException extends Exception * Create a new exception for missing route parameters. * * @param \Illuminate\Routing\Route $route + * @param array $parameters * @return static */ - public static function forMissingParameters($route) + public static function forMissingParameters(Route $route, array $parameters = []) { - return new static("Missing required parameters for [Route: {$route->getName()}] [URI: {$route->uri()}]."); + $parameterLabel = Str::plural('parameter', count($parameters)); + + $message = sprintf( + 'Missing required %s for [Route: %s] [URI: %s]', + $parameterLabel, + $route->getName(), + $route->uri() + ); + + if (count($parameters) > 0) { + $message .= sprintf(' [Missing %s: %s]', $parameterLabel, implode(', ', $parameters)); + } + + $message .= '.'; + + return new static($message); } } diff --git a/src/Illuminate/Routing/Middleware/SubstituteBindings.php b/src/Illuminate/Routing/Middleware/SubstituteBindings.php index 57adde76e915..d5f49ea91d9c 100644 --- a/src/Illuminate/Routing/Middleware/SubstituteBindings.php +++ b/src/Illuminate/Routing/Middleware/SubstituteBindings.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Contracts\Routing\Registrar; +use Illuminate\Database\Eloquent\ModelNotFoundException; class SubstituteBindings { @@ -34,9 +35,17 @@ public function __construct(Registrar $router) */ public function handle($request, Closure $next) { - $this->router->substituteBindings($route = $request->route()); + try { + $this->router->substituteBindings($route = $request->route()); - $this->router->substituteImplicitBindings($route); + $this->router->substituteImplicitBindings($route); + } catch (ModelNotFoundException $exception) { + if ($route->getMissing()) { + return $route->getMissing()($request); + } + + throw $exception; + } return $next($request); } diff --git a/src/Illuminate/Routing/PendingResourceRegistration.php b/src/Illuminate/Routing/PendingResourceRegistration.php index 3b6c97e2042d..59e4b8f0b78f 100644 --- a/src/Illuminate/Routing/PendingResourceRegistration.php +++ b/src/Illuminate/Routing/PendingResourceRegistration.php @@ -195,6 +195,19 @@ public function shallow($shallow = true) return $this; } + /** + * Define the callable that should be invoked on a missing model exception. + * + * @param callable $callback + * @return $this + */ + public function missing($callback) + { + $this->options['missing'] = $callback; + + return $this; + } + /** * Indicate that the resource routes should be scoped using the given binding fields. * diff --git a/src/Illuminate/Routing/ResourceRegistrar.php b/src/Illuminate/Routing/ResourceRegistrar.php index c32aa023b4bd..c32de58c291f 100644 --- a/src/Illuminate/Routing/ResourceRegistrar.php +++ b/src/Illuminate/Routing/ResourceRegistrar.php @@ -184,6 +184,8 @@ protected function addResourceIndex($name, $base, $controller, $options) { $uri = $this->getResourceUri($name); + unset($options['missing']); + $action = $this->getResourceAction($name, $controller, 'index', $options); return $this->router->get($uri, $action); @@ -202,6 +204,8 @@ protected function addResourceCreate($name, $base, $controller, $options) { $uri = $this->getResourceUri($name).'/'.static::$verbs['create']; + unset($options['missing']); + $action = $this->getResourceAction($name, $controller, 'create', $options); return $this->router->get($uri, $action); @@ -220,6 +224,8 @@ protected function addResourceStore($name, $base, $controller, $options) { $uri = $this->getResourceUri($name); + unset($options['missing']); + $action = $this->getResourceAction($name, $controller, 'store', $options); return $this->router->post($uri, $action); @@ -421,6 +427,10 @@ protected function getResourceAction($resource, $controller, $method, $options) $action['where'] = $options['wheres']; } + if (isset($options['missing'])) { + $action['missing'] = $options['missing']; + } + return $action; } diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index 37ba1aaa4236..b56e735a7fa1 100755 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -746,6 +746,8 @@ public function getPrefix() */ public function prefix($prefix) { + $prefix = $prefix ?? ''; + $this->updatePrefixOnAction($prefix); $uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/'); @@ -933,6 +935,34 @@ public function setAction(array $action) return $this; } + /** + * Get the value of the action that should be taken on a missing model exception. + * + * @return \Closure|null + */ + public function getMissing() + { + $missing = $this->action['missing'] ?? null; + + return is_string($missing) && + Str::startsWith($missing, 'C:32:"Opis\\Closure\\SerializableClosure') + ? unserialize($missing) + : $missing; + } + + /** + * Define the callable that should be invoked on a missing model exception. + * + * @param \Closure $missing + * @return $this + */ + public function missing($missing) + { + $this->action['missing'] = $missing; + + return $this; + } + /** * Get all middleware, including the ones from the controller. * @@ -1167,8 +1197,10 @@ public function prepareForSerialization() { if ($this->action['uses'] instanceof Closure) { $this->action['uses'] = serialize(new SerializableClosure($this->action['uses'])); + } - // throw new LogicException("Unable to prepare route [{$this->uri}] for serialization. Uses Closure."); + if (isset($this->action['missing']) && $this->action['missing'] instanceof Closure) { + $this->action['missing'] = serialize(new SerializableClosure($this->action['missing'])); } $this->compileRoute(); diff --git a/src/Illuminate/Routing/RouteAction.php b/src/Illuminate/Routing/RouteAction.php index 5a8188e27bbd..74035d4ce064 100644 --- a/src/Illuminate/Routing/RouteAction.php +++ b/src/Illuminate/Routing/RouteAction.php @@ -96,7 +96,7 @@ protected static function makeInvokable($action) } /** - * Determine if the given array actions contains a serialized Closure. + * Determine if the given array actions contain a serialized Closure. * * @param array $action * @return bool diff --git a/src/Illuminate/Routing/RouteUrlGenerator.php b/src/Illuminate/Routing/RouteUrlGenerator.php index 5cc03c1e246c..5f1248966c77 100644 --- a/src/Illuminate/Routing/RouteUrlGenerator.php +++ b/src/Illuminate/Routing/RouteUrlGenerator.php @@ -87,8 +87,8 @@ public function to($route, $parameters = [], $absolute = false) $route ), $parameters); - if (preg_match('/\{.*?\}/', $uri)) { - throw UrlGenerationException::forMissingParameters($route); + if (preg_match_all('/{(.*?)}/', $uri, $matchedMissingParameters)) { + throw UrlGenerationException::forMissingParameters($route, $matchedMissingParameters[1]); } // Once we have ensured that there are no missing parameters in the URI we will encode diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index f1b61dc74dcf..1aa62f0c64a5 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -18,6 +18,7 @@ use Illuminate\Routing\Events\RouteMatched; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use Illuminate\Support\Traits\Macroable; use JsonSerializable; use Psr\Http\Message\ResponseInterface as PsrResponseInterface; @@ -645,6 +646,8 @@ protected function findRoute($request) { $this->current = $route = $this->routes->match($request); + $route->setContainer($this->container); + $this->container->instance(Route::class, $route); return $route; @@ -711,7 +714,13 @@ public function gatherRouteMiddleware(Route $route) })->flatten()->reject(function ($name) use ($excluded) { if (empty($excluded)) { return false; - } elseif (in_array($name, $excluded, true)) { + } + + if ($name instanceof Closure) { + return false; + } + + if (in_array($name, $excluded, true)) { return true; } @@ -769,6 +778,8 @@ public static function toResponse($request, $response) $response = (new HttpFoundationFactory)->createResponse($response); } elseif ($response instanceof Model && $response->wasRecentlyCreated) { $response = new JsonResponse($response, 201); + } elseif ($response instanceof Stringable) { + $response = new Response($response->__toString(), 200, ['Content-Type' => 'text/html']); } elseif (! $response instanceof SymfonyResponse && ($response instanceof Arrayable || $response instanceof Jsonable || @@ -944,6 +955,18 @@ public function pushMiddlewareToGroup($group, $middleware) return $this; } + /** + * Flush the router's middleware groups. + * + * @return $this + */ + public function flushMiddlewareGroups() + { + $this->middlewareGroups = []; + + return $this; + } + /** * Add a new route parameter binder. * @@ -1270,6 +1293,19 @@ public static function uniqueMiddleware(array $middleware) return $result; } + /** + * Set the container instance used by the router. + * + * @param \Illuminate\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + + return $this; + } + /** * Dynamically handle calls into the router instance. * diff --git a/src/Illuminate/Session/Middleware/StartSession.php b/src/Illuminate/Session/Middleware/StartSession.php index c702d920473b..e7d2daa22315 100644 --- a/src/Illuminate/Session/Middleware/StartSession.php +++ b/src/Illuminate/Session/Middleware/StartSession.php @@ -59,9 +59,9 @@ public function handle($request, Closure $next) if ($this->manager->shouldBlock() || ($request->route() instanceof Route && $request->route()->locksFor())) { return $this->handleRequestWhileBlocking($request, $session, $next); - } else { - return $this->handleStatefulRequest($request, $session, $next); } + + return $this->handleStatefulRequest($request, $session, $next); } /** diff --git a/src/Illuminate/Support/Carbon.php b/src/Illuminate/Support/Carbon.php index 9383c3fd897d..004b27b0751e 100644 --- a/src/Illuminate/Support/Carbon.php +++ b/src/Illuminate/Support/Carbon.php @@ -3,8 +3,16 @@ namespace Illuminate\Support; use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; class Carbon extends BaseCarbon { - // + /** + * {@inheritdoc} + */ + public static function setTestNow($testNow = null) + { + BaseCarbon::setTestNow($testNow); + BaseCarbonImmutable::setTestNow($testNow); + } } diff --git a/src/Illuminate/Support/ConfigurationUrlParser.php b/src/Illuminate/Support/ConfigurationUrlParser.php index 946252fb6d15..be54b9a83d5b 100644 --- a/src/Illuminate/Support/ConfigurationUrlParser.php +++ b/src/Illuminate/Support/ConfigurationUrlParser.php @@ -170,7 +170,7 @@ protected function parseStringsToNativeTypes($value) } /** - * Get all of the current drivers aliases. + * Get all of the current drivers' aliases. * * @return array */ diff --git a/src/Illuminate/Support/DateFactory.php b/src/Illuminate/Support/DateFactory.php index 72f22231dbf0..e1d0ca14cda8 100644 --- a/src/Illuminate/Support/DateFactory.php +++ b/src/Illuminate/Support/DateFactory.php @@ -217,7 +217,7 @@ public function __call($method, $parameters) return $dateClass::$method(...$parameters); } - // If that fails, create the date with the default class.. + // If that fails, create the date with the default class... $date = $defaultClassName::$method(...$parameters); // If the configured class has an "instance" method, we'll try to pass our date into there... diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 63d8709143d7..b186d3284e9f 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -7,6 +7,7 @@ * @method static \Illuminate\Support\ServiceProvider register(\Illuminate\Support\ServiceProvider|string $provider, bool $force = false) * @method static \Illuminate\Support\ServiceProvider resolveProvider(string $provider) * @method static array getProviders(\Illuminate\Support\ServiceProvider|string $provider) + * @method static mixed make($abstract, array $parameters = []) * @method static bool configurationIsCached() * @method static bool hasBeenBootstrapped() * @method static bool isDownForMaintenance() @@ -14,7 +15,7 @@ * @method static bool runningInConsole() * @method static bool runningUnitTests() * @method static bool shouldSkipMiddleware() - * @method static string basePath() + * @method static string basePath(string $path = '') * @method static string bootstrapPath(string $path = '') * @method static string configPath(string $path = '') * @method static string databasePath(string $path = '') diff --git a/src/Illuminate/Support/Facades/Auth.php b/src/Illuminate/Support/Facades/Auth.php index 02ce2cd76b4c..eb9a05d2ed93 100755 --- a/src/Illuminate/Support/Facades/Auth.php +++ b/src/Illuminate/Support/Facades/Auth.php @@ -24,6 +24,7 @@ * @method static int|string|null id() * @method static void login(\Illuminate\Contracts\Auth\Authenticatable $user, bool $remember = false) * @method static void logout() + * @method static void logoutCurrentDevice() * @method static void setUser(\Illuminate\Contracts\Auth\Authenticatable $user) * @method static void shouldUse(string $name); * diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index ec2c3f7ddc13..ec95d86243d6 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -16,12 +16,12 @@ * @method static bool|mixed getCommandHandler($command) * @method static mixed dispatch($command) * @method static mixed dispatchNow($command, $handler = null) - * @method static void assertDispatched(string $command, callable|int $callback = null) + * @method static void assertDispatched(string|\Closure $command, callable|int $callback = null) * @method static void assertDispatchedTimes(string $command, int $times = 1) - * @method static void assertNotDispatched(string $command, callable $callback = null) - * @method static void assertDispatchedAfterResponse(string $command, callable|int $callback = null) + * @method static void assertNotDispatched(string|\Closure $command, callable|int $callback = null) + * @method static void assertDispatchedAfterResponse(string|\Closure $command, callable|int $callback = null) * @method static void assertDispatchedAfterResponseTimes(string $command, int $times = 1) - * @method static void assertNotDispatchedAfterResponse(string $command, callable $callback = null) + * @method static void assertNotDispatchedAfterResponse(string|\Closure $command, callable $callback = null) * @method static void assertBatched(callable $callback) * * @see \Illuminate\Contracts\Bus\Dispatcher diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 7986e3cf863c..997f790c0dd3 100755 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -7,10 +7,12 @@ * @method static \Illuminate\Database\ConnectionInterface connection(string $name = null) * @method static \Illuminate\Database\Query\Builder table(string $table, string $as = null) * @method static \Illuminate\Database\Query\Expression raw($value) + * @method static array getQueryLog() * @method static array prepareBindings(array $bindings) * @method static array pretend(\Closure $callback) * @method static array select(string $query, array $bindings = [], bool $useReadPdo = true) * @method static bool insert(string $query, array $bindings = []) + * @method static bool logging() * @method static bool statement(string $query, array $bindings = []) * @method static bool unprepared(string $query) * @method static int affectingStatement(string $query, array $bindings = []) @@ -20,8 +22,12 @@ * @method static mixed selectOne(string $query, array $bindings = [], bool $useReadPdo = true) * @method static mixed transaction(\Closure $callback, int $attempts = 1) * @method static string getDefaultConnection() + * @method static void afterCommit(\Closure $callback) * @method static void beginTransaction() * @method static void commit() + * @method static void enableQueryLog() + * @method static void disableQueryLog() + * @method static void flushQueryLog() * @method static void listen(\Closure $callback) * @method static void rollBack(int $toLevel = null) * @method static void setDefaultConnection(string $name) diff --git a/src/Illuminate/Support/Facades/Event.php b/src/Illuminate/Support/Facades/Event.php index d70d09334a1d..9d66ffa25650 100755 --- a/src/Illuminate/Support/Facades/Event.php +++ b/src/Illuminate/Support/Facades/Event.php @@ -13,13 +13,13 @@ * @method static array|null dispatch(string|object $event, mixed $payload = [], bool $halt = false) * @method static array|null until(string|object $event, mixed $payload = []) * @method static bool hasListeners(string $eventName) - * @method static void assertDispatched(string $event, callable|int $callback = null) + * @method static void assertDispatched(string|\Closure $event, callable|int $callback = null) * @method static void assertDispatchedTimes(string $event, int $times = 1) - * @method static void assertNotDispatched(string $event, callable|int $callback = null) + * @method static void assertNotDispatched(string|\Closure $event, callable|int $callback = null) * @method static void flush(string $event) * @method static void forget(string $event) * @method static void forgetPushed() - * @method static void listen(string|array $events, \Closure|string $listener = null) + * @method static void listen(\Closure|string|array $events, \Closure|string $listener = null) * @method static void push(string $event, array $payload = []) * @method static void subscribe(object|string $subscriber) * diff --git a/src/Illuminate/Support/Facades/File.php b/src/Illuminate/Support/Facades/File.php index 2dfad0b2fde2..13cfdde63cfd 100755 --- a/src/Illuminate/Support/Facades/File.php +++ b/src/Illuminate/Support/Facades/File.php @@ -40,6 +40,7 @@ * @method static string|false mimeType(string $path) * @method static void ensureDirectoryExists(string $path, int $mode = 0755, bool $recursive = true) * @method static void link(string $target, string $link) + * @method static \Illuminate\Support\LazyCollection lines(string $path) * @method static void relativeLink(string $target, string $link) * @method static void replace(string $path, string $content) * diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 9a81b017d7f3..426d574789c5 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -18,6 +18,7 @@ * @method static \Illuminate\Http\Client\PendingRequest bodyFormat(string $format) * @method static \Illuminate\Http\Client\PendingRequest contentType(string $contentType) * @method static \Illuminate\Http\Client\PendingRequest retry(int $times, int $sleep = 0) + * @method static \Illuminate\Http\Client\PendingRequest sink($to) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) * @method static \Illuminate\Http\Client\PendingRequest timeout(int $seconds) * @method static \Illuminate\Http\Client\PendingRequest withBasicAuth(string $username, string $password) @@ -29,6 +30,8 @@ * @method static \Illuminate\Http\Client\PendingRequest withToken(string $token, string $type = 'Bearer') * @method static \Illuminate\Http\Client\PendingRequest withoutRedirecting() * @method static \Illuminate\Http\Client\PendingRequest withoutVerifying() + * @method static \Illuminate\Http\Client\PendingRequest dump() + * @method static \Illuminate\Http\Client\PendingRequest dd() * @method static \Illuminate\Http\Client\Response delete(string $url, array $data = []) * @method static \Illuminate\Http\Client\Response get(string $url, array $query = []) * @method static \Illuminate\Http\Client\Response head(string $url, array $query = []) diff --git a/src/Illuminate/Support/Facades/Mail.php b/src/Illuminate/Support/Facades/Mail.php index 887629d67950..36796e752e55 100755 --- a/src/Illuminate/Support/Facades/Mail.php +++ b/src/Illuminate/Support/Facades/Mail.php @@ -21,8 +21,8 @@ * @method static void assertNotSent(string $mailable, callable|int $callback = null) * @method static void assertNothingQueued() * @method static void assertNothingSent() - * @method static void assertQueued(string $mailable, callable|int $callback = null) - * @method static void assertSent(string $mailable, callable|int $callback = null) + * @method static void assertQueued(string|\Closure $mailable, callable|int $callback = null) + * @method static void assertSent(string|\Closure $mailable, callable|int $callback = null) * @method static void raw(string $text, $callback) * @method static void plain(string $view, array $data, $callback) * @method static void html(string $html, $callback) diff --git a/src/Illuminate/Support/Facades/Notification.php b/src/Illuminate/Support/Facades/Notification.php index b3e858978c5c..8ab683eaf74b 100644 --- a/src/Illuminate/Support/Facades/Notification.php +++ b/src/Illuminate/Support/Facades/Notification.php @@ -11,9 +11,9 @@ * @method static \Illuminate\Support\Collection sent(mixed $notifiable, string $notification, callable $callback = null) * @method static bool hasSent(mixed $notifiable, string $notification) * @method static mixed channel(string|null $name = null) - * @method static void assertNotSentTo(mixed $notifiable, string $notification, callable $callback = null) + * @method static void assertNotSentTo(mixed $notifiable, string|\Closure $notification, callable $callback = null) * @method static void assertNothingSent() - * @method static void assertSentTo(mixed $notifiable, string $notification, callable $callback = null) + * @method static void assertSentTo(mixed $notifiable, string|\Closure $notification, callable $callback = null) * @method static void assertSentToTimes(mixed $notifiable, string $notification, int $times = 1) * @method static void assertTimesSent(int $expectedCount, string $notification) * @method static void send(\Illuminate\Support\Collection|array|mixed $notifiables, $notification) diff --git a/src/Illuminate/Support/Facades/ParallelTesting.php b/src/Illuminate/Support/Facades/ParallelTesting.php new file mode 100644 index 000000000000..c3976113501f --- /dev/null +++ b/src/Illuminate/Support/Facades/ParallelTesting.php @@ -0,0 +1,26 @@ +get('filesystems.default'); - (new Filesystem)->cleanDirectory( - $root = storage_path('framework/testing/disks/'.$disk) - ); + $root = storage_path('framework/testing/disks/'.$disk); + + if ($token = ParallelTesting::token()) { + $root = "{$root}_test_{$token}"; + } + + (new Filesystem)->cleanDirectory($root); static::set($disk, $fake = static::createLocalDriver(array_merge($config, [ 'root' => $root, diff --git a/src/Illuminate/Support/Facades/URL.php b/src/Illuminate/Support/Facades/URL.php index 9e11006d8697..7d9941d7c027 100755 --- a/src/Illuminate/Support/Facades/URL.php +++ b/src/Illuminate/Support/Facades/URL.php @@ -5,11 +5,13 @@ /** * @method static \Illuminate\Contracts\Routing\UrlGenerator setRootControllerNamespace(string $rootNamespace) * @method static bool hasValidSignature(\Illuminate\Http\Request $request, bool $absolute = true) - * @method static string action(string $action, $parameters = [], bool $absolute = true) + * @method static string action(string|array $action, $parameters = [], bool $absolute = true) * @method static string asset(string $path, bool $secure = null) * @method static string secureAsset(string $path) * @method static string current() * @method static string full() + * @method static void macro(string $name, object|callable $macro) + * @method static void mixin(object $mixin, bool $replace = true) * @method static string previous($fallback = false) * @method static string route(string $name, $parameters = [], bool $absolute = true) * @method static string secure(string $path, array $parameters = []) diff --git a/src/Illuminate/Support/Facades/View.php b/src/Illuminate/Support/Facades/View.php index b85b086c5547..9b66464c9d96 100755 --- a/src/Illuminate/Support/Facades/View.php +++ b/src/Illuminate/Support/Facades/View.php @@ -4,7 +4,7 @@ /** * @method static \Illuminate\Contracts\View\Factory addNamespace(string $namespace, string|array $hints) - * @method static \Illuminate\Contracts\View\Factory first(array $views, \Illuminate\Contracts\Support\Arrayable|array $data = [], array $mergeData = []) + * @method static \Illuminate\Contracts\View\View first(array $views, \Illuminate\Contracts\Support\Arrayable|array $data = [], array $mergeData = []) * @method static \Illuminate\Contracts\View\Factory replaceNamespace(string $namespace, string|array $hints) * @method static \Illuminate\Contracts\View\View file(string $path, array $data = [], array $mergeData = []) * @method static \Illuminate\Contracts\View\View make(string $view, array $data = [], array $mergeData = []) diff --git a/src/Illuminate/Support/Manager.php b/src/Illuminate/Support/Manager.php index 0150cc3c9637..e5f832d79428 100755 --- a/src/Illuminate/Support/Manager.php +++ b/src/Illuminate/Support/Manager.php @@ -144,6 +144,39 @@ public function getDrivers() return $this->drivers; } + /** + * Get the container instance used by the manager. + * + * @return \Illuminate\Contracts\Container\Container + */ + public function getContainer() + { + return $this->container; + } + + /** + * Set the container instance used by the manager. + * + * @param \Illuminate\Contracts\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + + return $this; + } + + /** + * Forget all of the resolved driver instances. + * + * @return $this + */ + public function forgetDrivers() + { + $this->drivers = []; + } + /** * Dynamically call the default driver instance. * diff --git a/src/Illuminate/Support/Pluralizer.php b/src/Illuminate/Support/Pluralizer.php index e7539a99aff2..5babd0e0e153 100755 --- a/src/Illuminate/Support/Pluralizer.php +++ b/src/Illuminate/Support/Pluralizer.php @@ -69,7 +69,7 @@ class Pluralizer */ public static function plural($value, $count = 2) { - if ((int) abs($count) === 1 || static::uncountable($value)) { + if ((int) abs($count) === 1 || static::uncountable($value) || preg_match('/^(.*)[A-Za-z0-9\x{0080}-\x{FFFF}]$/u', $value) == 0) { return $value; } diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index bd54885d9d1b..e8e9975b47d8 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -3,6 +3,7 @@ namespace Illuminate\Support; use Illuminate\Support\Traits\Macroable; +use League\CommonMark\GithubFlavoredMarkdownConverter; use Ramsey\Uuid\Codec\TimestampFirstCombCodec; use Ramsey\Uuid\Generator\CombGenerator; use Ramsey\Uuid\Uuid; @@ -376,6 +377,20 @@ public static function words($value, $words = 100, $end = '...') return rtrim($matches[0]).$end; } + /** + * Converts GitHub flavored Markdown into HTML. + * + * @param string $string + * @param array $options + * @return string + */ + public static function markdown($string, array $options = []) + { + $converter = new GithubFlavoredMarkdownConverter($options); + + return $converter->convertToHtml($string); + } + /** * Pad both sides of a string with another. * @@ -507,7 +522,7 @@ public static function replaceArray($search, array $replace, $subject) */ public static function replaceFirst($search, $replace, $subject) { - if ($search == '') { + if ($search === '') { return $subject; } @@ -543,6 +558,23 @@ public static function replaceLast($search, $replace, $subject) return $subject; } + /** + * Remove any occurrence of the given string in the subject. + * + * @param string|array $search + * @param string $subject + * @param bool $caseSensitive + * @return string + */ + public static function remove($search, $subject, $caseSensitive = true) + { + $subject = $caseSensitive + ? str_replace($search, '', $subject) + : str_ireplace($search, '', $subject); + + return $subject; + } + /** * Begin a string with a single instance of a given value. * @@ -681,7 +713,7 @@ public static function studly($value) } /** - * Returns the portion of string specified by the start and length parameters. + * Returns the portion of the string specified by the start and length parameters. * * @param string $string * @param int $start @@ -745,7 +777,7 @@ public static function orderedUuid() return call_user_func(static::$uuidFactory); } - $factory = new UuidFactory(); + $factory = new UuidFactory; $factory->setRandomGenerator(new CombGenerator( $factory->getRandomGenerator(), diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index c24d19e6a923..de9c8b69833a 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -4,11 +4,13 @@ use Closure; use Illuminate\Support\Traits\Macroable; +use Illuminate\Support\Traits\Tappable; +use JsonSerializable; use Symfony\Component\VarDumper\VarDumper; -class Stringable +class Stringable implements JsonSerializable { - use Macroable; + use Macroable, Tappable; /** * The underlying string value. @@ -318,11 +320,22 @@ public function lower() return new static(Str::lower($this->value)); } + /** + * Convert GitHub flavored Markdown into HTML. + * + * @param array $options + * @return static + */ + public function markdown(array $options = []) + { + return new static(Str::markdown($this->value, $options)); + } + /** * Get the string matching the given pattern. * * @param string $pattern - * @return static|null + * @return static */ public function match($pattern) { @@ -352,6 +365,17 @@ public function matchAll($pattern) return collect($matches[1] ?? $matches[0]); } + /** + * Determine if the string matches the given pattern. + * + * @param string $pattern + * @return bool + */ + public function test($pattern) + { + return $this->match($pattern)->isNotEmpty(); + } + /** * Pad both sides of the string with another. * @@ -399,6 +423,17 @@ public function parseCallback($default = null) return Str::parseCallback($this->value, $default); } + /** + * Call the given callback and return a new string. + * + * @param callable $callback + * @return static + */ + public function pipe(callable $callback) + { + return new static(call_user_func($callback, $this)); + } + /** * Get the plural form of an English word. * @@ -432,6 +467,18 @@ public function prepend(...$values) return new static(implode('', $values).$this->value); } + /** + * Remove any occurrence of the given string in the subject. + * + * @param string|array $search + * @param bool $caseSensitive + * @return static + */ + public function remove($search, $caseSensitive = true) + { + return new static(Str::remove($search, $this->value, $caseSensitive)); + } + /** * Replace the given value in the given string. * @@ -583,7 +630,7 @@ public function studly() } /** - * Returns the portion of string specified by the start and length parameters. + * Returns the portion of the string specified by the start and length parameters. * * @param int $start * @param int|null $length @@ -722,6 +769,16 @@ public function dd() exit(1); } + /** + * Convert the object to a string when JSON encoded. + * + * @return string + */ + public function jsonSerialize() + { + return $this->__toString(); + } + /** * Proxy dynamic properties onto methods. * diff --git a/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php b/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php index 55681f4d5534..d9661334ce01 100644 --- a/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php @@ -33,6 +33,7 @@ public function get($limit, $before) */ public function find(string $batchId) { + // } /** @@ -68,6 +69,7 @@ public function store(PendingBatch $batch) */ public function incrementTotalJobs(string $batchId, int $amount) { + // } /** @@ -102,6 +104,7 @@ public function incrementFailedJobs(string $batchId, string $jobId) */ public function markAsFinished(string $batchId) { + // } /** @@ -112,6 +115,7 @@ public function markAsFinished(string $batchId) */ public function cancel(string $batchId) { + // } /** @@ -122,6 +126,7 @@ public function cancel(string $batchId) */ public function delete(string $batchId) { + // } /** diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index 31a08d3548b0..82f3dda5d129 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -200,6 +200,14 @@ public function assertChained(array $expectedChain) if ($command instanceof Closure) { [$command, $callback] = [$this->firstClosureParameterType($command), $command]; + } elseif (! is_string($command)) { + $instance = $command; + + $command = get_class($instance); + + $callback = function ($job) use ($instance) { + return serialize($this->resetChainPropertiesToDefaults($job)) === serialize($instance); + }; } PHPUnit::assertTrue( @@ -217,6 +225,22 @@ public function assertChained(array $expectedChain) : $this->assertDispatchedWithChainOfClasses($command, $expectedChain, $callback); } + /** + * Reset the chain properties to their default values on the job. + * + * @param mixed $job + * @return mixed + */ + protected function resetChainPropertiesToDefaults($job) + { + return tap(clone $job, function ($job) { + $job->chainConnection = null; + $job->chainQueue = null; + $job->chainCatchCallbacks = null; + $job->chained = []; + }); + } + /** * Assert if a job was dispatched with an empty chain based on a truth-test callback. * @@ -493,6 +517,7 @@ public function chain($jobs) */ public function findBatch(string $batchId) { + // } /** @@ -520,7 +545,7 @@ public function recordPendingBatch(PendingBatch $pendingBatch) } /** - * Determine if an command should be faked or actually dispatched. + * Determine if a command should be faked or actually dispatched. * * @param mixed $command * @return bool diff --git a/src/Illuminate/Support/Testing/Fakes/EventFake.php b/src/Illuminate/Support/Testing/Fakes/EventFake.php index 84f67482ebe3..ed5014f15519 100644 --- a/src/Illuminate/Support/Testing/Fakes/EventFake.php +++ b/src/Illuminate/Support/Testing/Fakes/EventFake.php @@ -7,6 +7,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Traits\ReflectsClosures; use PHPUnit\Framework\Assert as PHPUnit; +use ReflectionFunction; class EventFake implements Dispatcher { @@ -47,6 +48,38 @@ public function __construct(Dispatcher $dispatcher, $eventsToFake = []) $this->eventsToFake = Arr::wrap($eventsToFake); } + /** + * Assert if an event has a listener attached to it. + * + * @param string $expectedEvent + * @param string $expectedListener + * @return void + */ + public function assertListening($expectedEvent, $expectedListener) + { + foreach ($this->dispatcher->getListeners($expectedEvent) as $listenerClosure) { + $actualListener = (new ReflectionFunction($listenerClosure)) + ->getStaticVariables()['listener']; + + if ($actualListener === $expectedListener || + ($actualListener instanceof Closure && + $expectedListener === Closure::class)) { + PHPUnit::assertTrue(true); + + return; + } + } + + PHPUnit::assertTrue( + false, + sprintf( + 'Event [%s] does not have the [%s] listener attached to it', + $expectedEvent, + print_r($expectedListener, true) + ) + ); + } + /** * Assert if an event was dispatched based on a truth-test callback. * @@ -106,6 +139,21 @@ public function assertNotDispatched($event, $callback = null) ); } + /** + * Assert that no events were dispatched. + * + * @return void + */ + public function assertNothingDispatched() + { + $count = count(Arr::flatten($this->events)); + + PHPUnit::assertSame( + 0, $count, + "{$count} unexpected events were dispatched." + ); + } + /** * Get all of the events matching a truth-test callback. * @@ -263,7 +311,7 @@ public function forgetPushed() * * @param string|object $event * @param mixed $payload - * @return void + * @return array|null */ public function until($event, $payload = []) { diff --git a/src/Illuminate/Support/Testing/Fakes/MailFake.php b/src/Illuminate/Support/Testing/Fakes/MailFake.php index d299bb1c5a31..a42fe341f40e 100644 --- a/src/Illuminate/Support/Testing/Fakes/MailFake.php +++ b/src/Illuminate/Support/Testing/Fakes/MailFake.php @@ -276,7 +276,7 @@ protected function queuedMailablesOf($type) * Get a mailer instance by name. * * @param string|null $name - * @return \Illuminate\Mail\Mailer + * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null) { @@ -322,7 +322,7 @@ public function raw($text, $callback) /** * Send a new message using a view. * - * @param string|array $view + * @param \Illuminate\Contracts\Mail\Mailable|string|array $view * @param array $data * @param \Closure|string|null $callback * @return void diff --git a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php index cf3a25afd696..28526d592556 100644 --- a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php +++ b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php @@ -210,7 +210,7 @@ protected function notificationsFor($notifiable, $notification) */ public function send($notifiables, $notification) { - return $this->sendNow($notifiables, $notification); + $this->sendNow($notifiables, $notification); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php b/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php index c39012501ae6..52251301ceb9 100644 --- a/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php +++ b/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php @@ -22,11 +22,11 @@ public function __construct($mailer) * Send a new mailable message instance. * * @param \Illuminate\Contracts\Mail\Mailable $mailable - * @return mixed + * @return void */ public function send(Mailable $mailable) { - return $this->mailer->send($this->fill($mailable)); + $this->mailer->send($this->fill($mailable)); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index e83408fc0c74..64d6414fd81b 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -74,7 +74,7 @@ public function assertPushedOn($queue, $job, $callback = null) [$job, $callback] = [$this->firstClosureParameterType($job), $job]; } - return $this->assertPushed($job, function ($job, $pushedQueue) use ($callback, $queue) { + $this->assertPushed($job, function ($job, $pushedQueue) use ($callback, $queue) { if ($pushedQueue !== $queue) { return false; } diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 61c69eed0132..657c625c5eb3 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -42,6 +42,7 @@ }, "suggest": { "illuminate/filesystem": "Required to use the composer class (^8.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^1.3).", "ramsey/uuid": "Required to use Str::uuid() (^4.0).", "symfony/process": "Required to use the composer class (^5.1.4).", "symfony/var-dumper": "Required to use the dd function (^5.1.4).", diff --git a/src/Illuminate/Support/helpers.php b/src/Illuminate/Support/helpers.php index f44371bb469e..85486f6bbc55 100755 --- a/src/Illuminate/Support/helpers.php +++ b/src/Illuminate/Support/helpers.php @@ -101,7 +101,7 @@ function class_uses_recursive($class) /** * Encode HTML special characters in a string. * - * @param \Illuminate\Contracts\Support\DeferringDisplayableValue|\Illuminate\Contracts\Support\Htmlable|string $value + * @param \Illuminate\Contracts\Support\DeferringDisplayableValue|\Illuminate\Contracts\Support\Htmlable|string|null $value * @param bool $doubleEncode * @return string */ @@ -115,7 +115,7 @@ function e($value, $doubleEncode = true) return $value->toHtml(); } - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode); + return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8', $doubleEncode); } } @@ -157,7 +157,7 @@ function filled($value) */ function object_get($object, $key, $default = null) { - if (is_null($key) || trim($key) == '') { + if (is_null($key) || trim($key) === '') { return $object; } @@ -216,13 +216,13 @@ function preg_replace_array($pattern, array $replacements, $subject) * * @param int $times * @param callable $callback - * @param int $sleep + * @param int $sleepMilliseconds * @param callable|null $when * @return mixed * * @throws \Exception */ - function retry($times, callable $callback, $sleep = 0, $when = null) + function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null) { $attempts = 0; @@ -237,8 +237,8 @@ function retry($times, callable $callback, $sleep = 0, $when = null) throw $e; } - if ($sleep) { - usleep($sleep * 1000); + if ($sleepMilliseconds) { + usleep($sleepMilliseconds * 1000); } goto beginning; @@ -277,10 +277,14 @@ function tap($value, $callback = null) * * @throws \Throwable */ - function throw_if($condition, $exception, ...$parameters) + function throw_if($condition, $exception = 'RuntimeException', ...$parameters) { if ($condition) { - throw (is_string($exception) ? new $exception(...$parameters) : $exception); + if (is_string($exception) && class_exists($exception)) { + $exception = new $exception(...$parameters); + } + + throw is_string($exception) ? new RuntimeException($exception) : $exception; } return $condition; @@ -298,10 +302,14 @@ function throw_if($condition, $exception, ...$parameters) * * @throws \Throwable */ - function throw_unless($condition, $exception, ...$parameters) + function throw_unless($condition, $exception = 'RuntimeException', ...$parameters) { if (! $condition) { - throw (is_string($exception) ? new $exception(...$parameters) : $exception); + if (is_string($exception) && class_exists($exception)) { + $exception = new $exception(...$parameters); + } + + throw is_string($exception) ? new RuntimeException($exception) : $exception; } return $condition; @@ -317,7 +325,7 @@ function throw_unless($condition, $exception, ...$parameters) */ function trait_uses_recursive($trait) { - $traits = class_uses($trait); + $traits = class_uses($trait) ?: []; foreach ($traits as $trait) { $traits += trait_uses_recursive($trait); diff --git a/src/Illuminate/Testing/AssertableJsonString.php b/src/Illuminate/Testing/AssertableJsonString.php index 5a73afe50c7f..e36c84aa300d 100644 --- a/src/Illuminate/Testing/AssertableJsonString.php +++ b/src/Illuminate/Testing/AssertableJsonString.php @@ -276,8 +276,7 @@ public function assertSubset(array $data, $strict = false) /** * Reorder associative array keys to make it easy to compare arrays. * - * @param array $data - * + * @param array $data * @return array */ protected function reorderAssocKeys(array $data) diff --git a/src/Illuminate/Testing/Concerns/TestDatabases.php b/src/Illuminate/Testing/Concerns/TestDatabases.php new file mode 100644 index 000000000000..9e3198a0f166 --- /dev/null +++ b/src/Illuminate/Testing/Concerns/TestDatabases.php @@ -0,0 +1,179 @@ +whenNotUsingInMemoryDatabase(function ($database) { + if (ParallelTesting::option('recreate_databases')) { + Schema::dropDatabaseIfExists( + $this->testDatabase($database) + ); + } + }); + }); + + ParallelTesting::setUpTestCase(function ($testCase) { + $uses = array_flip(class_uses_recursive(get_class($testCase))); + + $databaseTraits = [ + Testing\DatabaseMigrations::class, + Testing\DatabaseTransactions::class, + Testing\RefreshDatabase::class, + ]; + + if (Arr::hasAny($uses, $databaseTraits)) { + $this->whenNotUsingInMemoryDatabase(function ($database) use ($uses) { + [$testDatabase, $created] = $this->ensureTestDatabaseExists($database); + + $this->switchToDatabase($testDatabase); + + if (isset($uses[Testing\DatabaseTransactions::class])) { + $this->ensureSchemaIsUpToDate(); + } + + if ($created) { + ParallelTesting::callSetUpTestDatabaseCallbacks($testDatabase); + } + }); + } + }); + } + + /** + * Ensure a test database exists and returns its name. + * + * @param string $database + * + * @return array + */ + protected function ensureTestDatabaseExists($database) + { + $testDatabase = $this->testDatabase($database); + + try { + $this->usingDatabase($testDatabase, function () { + Schema::hasTable('dummy'); + }); + } catch (QueryException $e) { + $this->usingDatabase($database, function () use ($testDatabase) { + Schema::dropDatabaseIfExists($testDatabase); + Schema::createDatabase($testDatabase); + }); + + return [$testDatabase, true]; + } + + return [$testDatabase, false]; + } + + /** + * Ensure the current database test schema is up to date. + * + * @return void + */ + protected function ensureSchemaIsUpToDate() + { + if (! static::$schemaIsUpToDate) { + Artisan::call('migrate'); + + static::$schemaIsUpToDate = true; + } + } + + /** + * Runs the given callable using the given database. + * + * @param string $database + * @param callable $callable + * @return void + */ + protected function usingDatabase($database, $callable) + { + $original = DB::getConfig('database'); + + try { + $this->switchToDatabase($database); + $callable(); + } finally { + $this->switchToDatabase($original); + } + } + + /** + * Apply the given callback when tests are not using in memory database. + * + * @param callable $callback + * @return void + */ + protected function whenNotUsingInMemoryDatabase($callback) + { + $database = DB::getConfig('database'); + + if ($database !== ':memory:') { + $callback($database); + } + } + + /** + * Switch to the given database. + * + * @param string $database + * @return void + */ + protected function switchToDatabase($database) + { + DB::purge(); + + $default = config('database.default'); + + $url = config("database.connections.{$default}.url"); + + if ($url) { + config()->set( + "database.connections.{$default}.url", + preg_replace('/^(.*)(\/[\w-]*)(\??.*)$/', "$1/{$database}$3", $url), + ); + } else { + config()->set( + "database.connections.{$default}.database", + $database, + ); + } + } + + /** + * Returns the test database name. + * + * @return string + */ + protected function testDatabase($database) + { + $token = ParallelTesting::token(); + + return "{$database}_test_{$token}"; + } +} diff --git a/src/Illuminate/Testing/Constraints/HasInDatabase.php b/src/Illuminate/Testing/Constraints/HasInDatabase.php index 73af3ddc13b7..039ee4425d99 100644 --- a/src/Illuminate/Testing/Constraints/HasInDatabase.php +++ b/src/Illuminate/Testing/Constraints/HasInDatabase.php @@ -3,6 +3,7 @@ namespace Illuminate\Testing\Constraints; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Expression; use PHPUnit\Framework\Constraint\Constraint; class HasInDatabase extends Constraint diff --git a/src/Illuminate/Testing/Fluent/AssertableJson.php b/src/Illuminate/Testing/Fluent/AssertableJson.php new file mode 100644 index 000000000000..3d2496fac71b --- /dev/null +++ b/src/Illuminate/Testing/Fluent/AssertableJson.php @@ -0,0 +1,151 @@ +path = $path; + $this->props = $props; + } + + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + protected function dotPath(string $key = ''): string + { + if (is_null($this->path)) { + return $key; + } + + return rtrim(implode('.', [$this->path, $key]), '.'); + } + + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + protected function prop(string $key = null) + { + return Arr::get($this->props, $key); + } + + /** + * Instantiate a new "scope" at the path of the given key. + * + * @param string $key + * @param \Closure $callback + * @return $this + */ + protected function scope(string $key, Closure $callback): self + { + $props = $this->prop($key); + $path = $this->dotPath($key); + + PHPUnit::assertIsArray($props, sprintf('Property [%s] is not scopeable.', $path)); + + $scope = new self($props, $path); + $callback($scope); + $scope->interacted(); + + return $this; + } + + /** + * Instantiate a new "scope" on the first child element. + * + * @param \Closure $callback + * @return $this + */ + public function first(Closure $callback): self + { + $props = $this->prop(); + + $path = $this->dotPath(); + + PHPUnit::assertNotEmpty($props, $path === '' + ? 'Cannot scope directly onto the first element of the root level because it is empty.' + : sprintf('Cannot scope directly onto the first element of property [%s] because it is empty.', $path) + ); + + $key = array_keys($props)[0]; + + $this->interactsWith($key); + + return $this->scope($key, $callback); + } + + /** + * Create a new instance from an array. + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + return new self($data); + } + + /** + * Create a new instance from a AssertableJsonString. + * + * @param \Illuminate\Testing\AssertableJsonString $json + * @return static + */ + public static function fromAssertableJsonString(AssertableJsonString $json): self + { + return self::fromArray($json->json()); + } + + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray() + { + return $this->props; + } +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php new file mode 100644 index 000000000000..f51d119074ae --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php @@ -0,0 +1,38 @@ +prop($prop)); + + return $this; + } + + /** + * Dumps the given props and exits. + * + * @param string|null $prop + * @return void + */ + public function dd(string $prop = null): void + { + dd($this->prop($prop)); + } + + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php new file mode 100644 index 000000000000..979b9afa3625 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -0,0 +1,186 @@ +dotPath(); + + PHPUnit::assertCount( + $key, + $this->prop(), + $path + ? sprintf('Property [%s] does not have the expected size.', $path) + : sprintf('Root level does not have the expected size.') + ); + + return $this; + } + + PHPUnit::assertCount( + $length, + $this->prop($key), + sprintf('Property [%s] does not have the expected size.', $this->dotPath($key)) + ); + + return $this; + } + + /** + * Ensure that the given prop exists. + * + * @param string|int $key + * @param int|\Closure|null $length + * @param \Closure|null $callback + * @return $this + */ + public function has($key, $length = null, Closure $callback = null): self + { + $prop = $this->prop(); + + if (is_int($key) && is_null($length)) { + return $this->count($key); + } + + PHPUnit::assertTrue( + Arr::has($prop, $key), + sprintf('Property [%s] does not exist.', $this->dotPath($key)) + ); + + $this->interactsWith($key); + + if (is_int($length) && ! is_null($callback)) { + return $this->has($key, function (self $scope) use ($length, $callback) { + return $scope->count($length) + ->first($callback) + ->etc(); + }); + } + + if (is_callable($length)) { + return $this->scope($key, $length); + } + + if (! is_null($length)) { + return $this->count($key, $length); + } + + return $this; + } + + /** + * Assert that all of the given props exist. + * + * @param array|string $key + * @return $this + */ + public function hasAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop => $count) { + if (is_int($prop)) { + $this->has($count); + } else { + $this->has($prop, $count); + } + } + + return $this; + } + + /** + * Assert that none of the given props exist. + * + * @param array|string $key + * @return $this + */ + public function missingAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop) { + $this->missing($prop); + } + + return $this; + } + + /** + * Assert that the given prop does not exist. + * + * @param string $key + * @return $this + */ + public function missing(string $key): self + { + PHPUnit::assertNotTrue( + Arr::has($this->prop(), $key), + sprintf('Property [%s] was found while it was expected to be missing.', $this->dotPath($key)) + ); + + return $this; + } + + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + abstract protected function dotPath(string $key = ''): string; + + /** + * Marks the property as interacted. + * + * @param string $key + * @return void + */ + abstract protected function interactsWith(string $key): void; + + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + abstract protected function prop(string $key = null); + + /** + * Instantiate a new "scope" at the path of the given key. + * + * @param string $key + * @param \Closure $callback + * @return $this + */ + abstract protected function scope(string $key, Closure $callback); + + /** + * Disables the interaction check. + * + * @return $this + */ + abstract public function etc(); + + /** + * Instantiate a new "scope" on the first element. + * + * @param \Closure $callback + * @return $this + */ + abstract public function first(Closure $callback); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php new file mode 100644 index 000000000000..15e7e9508f55 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php @@ -0,0 +1,67 @@ +interacted, true)) { + $this->interacted[] = $prop; + } + } + + /** + * Asserts that all properties have been interacted with. + * + * @return void + */ + public function interacted(): void + { + PHPUnit::assertSame( + [], + array_diff(array_keys($this->prop()), $this->interacted), + $this->path + ? sprintf('Unexpected properties were found in scope [%s].', $this->path) + : 'Unexpected properties were found on the root level.' + ); + } + + /** + * Disables the interaction check. + * + * @return $this + */ + public function etc(): self + { + $this->interacted = array_keys($this->prop()); + + return $this; + } + + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php new file mode 100644 index 000000000000..0872a6191f40 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -0,0 +1,150 @@ +has($key); + + $actual = $this->prop($key); + + if ($expected instanceof Closure) { + PHPUnit::assertTrue( + $expected(is_array($actual) ? Collection::make($actual) : $actual), + sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key)) + ); + + return $this; + } + + if ($expected instanceof Arrayable) { + $expected = $expected->toArray(); + } + + $this->ensureSorted($expected); + $this->ensureSorted($actual); + + PHPUnit::assertSame( + $expected, + $actual, + sprintf('Property [%s] does not match the expected value.', $this->dotPath($key)) + ); + + return $this; + } + + /** + * Asserts that all properties match their expected values. + * + * @param array $bindings + * @return $this + */ + public function whereAll(array $bindings): self + { + foreach ($bindings as $key => $value) { + $this->where($key, $value); + } + + return $this; + } + + /** + * Asserts that the property is of the expected type. + * + * @param string $key + * @param string|array $expected + * @return $this + */ + public function whereType(string $key, $expected): self + { + $this->has($key); + + $actual = $this->prop($key); + + if (! is_array($expected)) { + $expected = explode('|', $expected); + } + + PHPUnit::assertContains( + strtolower(gettype($actual)), + $expected, + sprintf('Property [%s] is not of expected type [%s].', $this->dotPath($key), implode('|', $expected)) + ); + + return $this; + } + + /** + * Asserts that all properties are of their expected types. + * + * @param array $bindings + * @return $this + */ + public function whereAllType(array $bindings): self + { + foreach ($bindings as $key => $value) { + $this->whereType($key, $value); + } + + return $this; + } + + /** + * Ensures that all properties are sorted the same way, recursively. + * + * @param mixed $value + * @return void + */ + protected function ensureSorted(&$value): void + { + if (! is_array($value)) { + return; + } + + foreach ($value as &$arg) { + $this->ensureSorted($arg); + } + + ksort($value); + } + + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + abstract protected function dotPath(string $key = ''): string; + + /** + * Ensure that the given prop exists. + * + * @param string $key + * @param null $value + * @param \Closure|null $scope + * @return $this + */ + abstract public function has(string $key, $value = null, Closure $scope = null); + + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/ParallelConsoleOutput.php b/src/Illuminate/Testing/ParallelConsoleOutput.php new file mode 100644 index 000000000000..7444be3b92d7 --- /dev/null +++ b/src/Illuminate/Testing/ParallelConsoleOutput.php @@ -0,0 +1,60 @@ +getVerbosity(), + $output->isDecorated(), + $output->getFormatter(), + ); + + $this->output = $output; + } + + /** + * Writes a message to the output. + * + * @param string|iterable $messages + * @param bool $newline + * @param int $options + * @return void + */ + public function write($messages, bool $newline = false, int $options = 0) + { + $messages = collect($messages)->filter(function ($message) { + return ! Str::contains($message, $this->ignore); + }); + + $this->output->write($messages->toArray(), $newline, $options); + } +} diff --git a/src/Illuminate/Testing/ParallelRunner.php b/src/Illuminate/Testing/ParallelRunner.php new file mode 100644 index 000000000000..802fe22ca30d --- /dev/null +++ b/src/Illuminate/Testing/ParallelRunner.php @@ -0,0 +1,152 @@ +options = $options; + + if ($output instanceof ConsoleOutput) { + $output = new ParallelConsoleOutput($output); + } + + $this->runner = new WrapperRunner($options, $output); + } + + /** + * Set the application resolver callback. + * + * @param \Closure|null $resolver + * @return void + */ + public static function resolveApplicationUsing($resolver) + { + static::$applicationResolver = $resolver; + } + + /** + * Runs the test suite. + * + * @return void + */ + public function run(): void + { + (new PhpHandler)->handle($this->options->configuration()->php()); + + $this->forEachProcess(function () { + ParallelTesting::callSetUpProcessCallbacks(); + }); + + try { + $this->runner->run(); + } finally { + $this->forEachProcess(function () { + ParallelTesting::callTearDownProcessCallbacks(); + }); + } + } + + /** + * Returns the highest exit code encountered throughout the course of test execution. + * + * @return int + */ + public function getExitCode(): int + { + return $this->runner->getExitCode(); + } + + /** + * Apply the given callback for each process. + * + * @param callable $callback + * @return void + */ + protected function forEachProcess($callback) + { + collect(range(1, $this->options->processes()))->each(function ($token) use ($callback) { + tap($this->createApplication(), function ($app) use ($callback, $token) { + ParallelTesting::resolveTokenUsing(function () use ($token) { + return $token; + }); + + $callback($app); + })->flush(); + }); + } + + /** + * Creates the application. + * + * @return \Illuminate\Contracts\Foundation\Application + */ + protected function createApplication() + { + $applicationResolver = static::$applicationResolver ?: function () { + if (trait_exists(\Tests\CreatesApplication::class)) { + $applicationCreator = new class { + use \Tests\CreatesApplication; + }; + + return $applicationCreator->createApplication(); + } elseif (file_exists(getcwd().'/bootstrap/app.php')) { + $app = require getcwd().'/bootstrap/app.php'; + + $app->make(Kernel::class)->bootstrap(); + + return $app; + } + + throw new RuntimeException('Parallel Runner unable to resolve application.'); + }; + + return call_user_func($applicationResolver); + } +} diff --git a/src/Illuminate/Testing/ParallelTesting.php b/src/Illuminate/Testing/ParallelTesting.php new file mode 100644 index 000000000000..11ebcfa89ed6 --- /dev/null +++ b/src/Illuminate/Testing/ParallelTesting.php @@ -0,0 +1,291 @@ +container = $container; + } + + /** + * Set a callback that should be used when resolving options. + * + * @param \Closure|null $callback + * @return void + */ + public function resolveOptionsUsing($resolver) + { + $this->optionsResolver = $resolver; + } + + /** + * Set a callback that should be used when resolving the unique process token. + * + * @param \Closure|null $callback + * @return void + */ + public function resolveTokenUsing($resolver) + { + $this->tokenResolver = $resolver; + } + + /** + * Register a "setUp" process callback. + * + * @param callable $callback + * @return void + */ + public function setUpProcess($callback) + { + $this->setUpProcessCallbacks[] = $callback; + } + + /** + * Register a "setUp" test case callback. + * + * @param callable $callback + * @return void + */ + public function setUpTestCase($callback) + { + $this->setUpTestCaseCallbacks[] = $callback; + } + + /** + * Register a "setUp" test database callback. + * + * @param callable $callback + * @return void + */ + public function setUpTestDatabase($callback) + { + $this->setUpTestDatabaseCallbacks[] = $callback; + } + + /** + * Register a "tearDown" process callback. + * + * @param callable $callback + * @return void + */ + public function tearDownProcess($callback) + { + $this->tearDownProcessCallbacks[] = $callback; + } + + /** + * Register a "tearDown" test case callback. + * + * @param callable $callback + * @return void + */ + public function tearDownTestCase($callback) + { + $this->tearDownTestCaseCallbacks[] = $callback; + } + + /** + * Call all of the "setUp" process callbacks. + * + * @return void + */ + public function callSetUpProcessCallbacks() + { + $this->whenRunningInParallel(function () { + foreach ($this->setUpProcessCallbacks as $callback) { + $this->container->call($callback, [ + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "setUp" test case callbacks. + * + * @param \Illuminate\Foundation\Testing\TestCase $testCase + * @return void + */ + public function callSetUpTestCaseCallbacks($testCase) + { + $this->whenRunningInParallel(function () use ($testCase) { + foreach ($this->setUpTestCaseCallbacks as $callback) { + $this->container->call($callback, [ + 'testCase' => $testCase, + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "setUp" test database callbacks. + * + * @param string $database + * @return void + */ + public function callSetUpTestDatabaseCallbacks($database) + { + $this->whenRunningInParallel(function () use ($database) { + foreach ($this->setUpTestDatabaseCallbacks as $callback) { + $this->container->call($callback, [ + 'database' => $database, + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "tearDown" process callbacks. + * + * @return void + */ + public function callTearDownProcessCallbacks() + { + $this->whenRunningInParallel(function () { + foreach ($this->tearDownProcessCallbacks as $callback) { + $this->container->call($callback, [ + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "tearDown" test case callbacks. + * + * @param \Illuminate\Foundation\Testing\TestCase $testCase + * @return void + */ + public function callTearDownTestCaseCallbacks($testCase) + { + $this->whenRunningInParallel(function () use ($testCase) { + foreach ($this->tearDownTestCaseCallbacks as $callback) { + $this->container->call($callback, [ + 'testCase' => $testCase, + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Get an parallel testing option. + * + * @param string $option + * @return mixed + */ + public function option($option) + { + $optionsResolver = $this->optionsResolver ?: function ($option) { + $option = 'LARAVEL_PARALLEL_TESTING_'.Str::upper($option); + + return $_SERVER[$option] ?? false; + }; + + return call_user_func($optionsResolver, $option); + } + + /** + * Gets a unique test token. + * + * @return int|false + */ + public function token() + { + return $token = $this->tokenResolver + ? call_user_func($this->tokenResolver) + : ($_SERVER['TEST_TOKEN'] ?? false); + } + + /** + * Apply the callback if tests are running in parallel. + * + * @param callable $callback + * @return void + */ + protected function whenRunningInParallel($callback) + { + if ($this->inParallel()) { + $callback(); + } + } + + /** + * Indicates if the current tests are been run in parallel. + * + * @return bool + */ + protected function inParallel() + { + return ! empty($_SERVER['LARAVEL_PARALLEL_TESTING']) && $this->token(); + } +} diff --git a/src/Illuminate/Testing/ParallelTestingServiceProvider.php b/src/Illuminate/Testing/ParallelTestingServiceProvider.php new file mode 100644 index 000000000000..20b900d2e58e --- /dev/null +++ b/src/Illuminate/Testing/ParallelTestingServiceProvider.php @@ -0,0 +1,38 @@ +app->runningInConsole()) { + $this->bootTestDatabase(); + } + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + if ($this->app->runningInConsole()) { + $this->app->singleton(ParallelTesting::class, function () { + return new ParallelTesting($this->app); + }); + } + } +} diff --git a/src/Illuminate/Testing/PendingCommand.php b/src/Illuminate/Testing/PendingCommand.php index 55ea307c82f3..7b90444bddd2 100644 --- a/src/Illuminate/Testing/PendingCommand.php +++ b/src/Illuminate/Testing/PendingCommand.php @@ -52,7 +52,7 @@ class PendingCommand protected $expectedExitCode; /** - * Determine if command has executed. + * Determine if the command has executed. * * @var bool */ @@ -157,12 +157,24 @@ public function doesntExpectOutput($output) */ public function expectsTable($headers, $rows, $tableStyle = 'default', array $columnStyles = []) { - $this->test->expectedTables[] = [ - 'headers' => (array) $headers, - 'rows' => $rows instanceof Arrayable ? $rows->toArray() : $rows, - 'tableStyle' => $tableStyle, - 'columnStyles' => $columnStyles, - ]; + $table = (new Table($output = new BufferedOutput)) + ->setHeaders((array) $headers) + ->setRows($rows instanceof Arrayable ? $rows->toArray() : $rows) + ->setStyle($tableStyle); + + foreach ($columnStyles as $columnIndex => $columnStyle) { + $table->setColumnStyle($columnIndex, $columnStyle); + } + + $table->render(); + + $lines = array_filter( + explode(PHP_EOL, $output->fetch()) + ); + + foreach ($lines as $line) { + $this->expectsOutput($line); + } return $this; } @@ -305,8 +317,6 @@ private function createABufferedOutputMock() ->shouldAllowMockingProtectedMethods() ->shouldIgnoreMissing(); - $this->applyTableOutputExpectations($mock); - foreach ($this->test->expectedOutput as $i => $output) { $mock->shouldReceive('doWrite') ->once() @@ -319,7 +329,6 @@ private function createABufferedOutputMock() foreach ($this->test->unexpectedOutput as $output => $displayed) { $mock->shouldReceive('doWrite') - ->once() ->ordered() ->with($output, Mockery::any()) ->andReturnUsing(function () use ($output) { @@ -330,38 +339,6 @@ private function createABufferedOutputMock() return $mock; } - /** - * Apply the output table expectations to the mock. - * - * @param \Mockery\MockInterface $mock - * @return void - */ - private function applyTableOutputExpectations($mock) - { - foreach ($this->test->expectedTables as $i => $consoleTable) { - $table = (new Table($output = new BufferedOutput)) - ->setHeaders($consoleTable['headers']) - ->setRows($consoleTable['rows']) - ->setStyle($consoleTable['tableStyle']); - - foreach ($consoleTable['columnStyles'] as $columnIndex => $columnStyle) { - $table->setColumnStyle($columnIndex, $columnStyle); - } - - $table->render(); - - $lines = array_filter( - explode(PHP_EOL, $output->fetch()) - ); - - foreach ($lines as $line) { - $this->expectsOutput($line); - } - - unset($this->test->expectedTables[$i]); - } - } - /** * Flush the expectations from the test case. * diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index e4bb3f8d6045..1d46a0ac5284 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -14,6 +14,7 @@ use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; +use Illuminate\Testing\Fluent\AssertableJson; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -234,7 +235,7 @@ public function assertHeader($headerName, $value = null) } /** - * Asserts that the response does not contains the given header. + * Asserts that the response does not contain the given header. * * @param string $headerName * @return $this @@ -358,7 +359,7 @@ public function assertCookieNotExpired($cookieName) } /** - * Asserts that the response does not contains the given cookie. + * Asserts that the response does not contain the given cookie. * * @param string $cookieName * @return $this @@ -507,13 +508,25 @@ public function assertDontSeeText($value, $escape = true) /** * Assert that the response is a superset of the given JSON. * - * @param array $data + * @param array|callable $value * @param bool $strict * @return $this */ - public function assertJson(array $data, $strict = false) + public function assertJson($value, $strict = false) { - $this->decodeResponseJson()->assertSubset($data, $strict); + $json = $this->decodeResponseJson(); + + if (is_array($value)) { + $json->assertSubset($value, $strict); + } else { + $assert = AssertableJson::fromAssertableJsonString($json); + + $value($assert); + + if (Arr::isAssoc($assert->toArray())) { + $assert->interacted(); + } + } return $this; } @@ -692,13 +705,13 @@ public function assertJsonMissingValidationErrors($keys = null, $responseKey = ' $json = $this->json(); - if (! array_key_exists($responseKey, $json)) { - PHPUnit::assertArrayNotHasKey($responseKey, $json); + if (! Arr::has($json, $responseKey)) { + PHPUnit::assertTrue(true); return $this; } - $errors = $json[$responseKey]; + $errors = Arr::get($json, $responseKey, []); if (is_null($keys) && count($errors) > 0) { PHPUnit::fail( diff --git a/src/Illuminate/Testing/composer.json b/src/Illuminate/Testing/composer.json index 9b15533bd7c8..2dfe3b64eac3 100644 --- a/src/Illuminate/Testing/composer.json +++ b/src/Illuminate/Testing/composer.json @@ -31,6 +31,7 @@ } }, "suggest": { + "brianium/paratest": "Required to run tests in parallel (^6.0).", "illuminate/console": "Required to assert console commands (^8.0).", "illuminate/database": "Required to assert databases (^8.0).", "illuminate/http": "Required to assert responses (^8.0).", diff --git a/src/Illuminate/Translation/FileLoader.php b/src/Illuminate/Translation/FileLoader.php index 17f6e59f0b0e..f359a8e5584d 100755 --- a/src/Illuminate/Translation/FileLoader.php +++ b/src/Illuminate/Translation/FileLoader.php @@ -164,6 +164,16 @@ public function addNamespace($namespace, $hint) $this->hints[$namespace] = $hint; } + /** + * Get an array of all the registered namespaces. + * + * @return array + */ + public function namespaces() + { + return $this->hints; + } + /** * Add a new JSON path to the loader. * @@ -176,12 +186,12 @@ public function addJsonPath($path) } /** - * Get an array of all the registered namespaces. + * Get an array of all the registered paths to JSON translation files. * * @return array */ - public function namespaces() + public function jsonPaths() { - return $this->hints; + return $this->jsonPaths; } } diff --git a/src/Illuminate/Validation/Concerns/FormatsMessages.php b/src/Illuminate/Validation/Concerns/FormatsMessages.php index e380b6e1807a..f433a5361eec 100644 --- a/src/Illuminate/Validation/Concerns/FormatsMessages.php +++ b/src/Illuminate/Validation/Concerns/FormatsMessages.php @@ -116,7 +116,7 @@ protected function getFromLocalArray($attribute, $lowerRule, $source = null) } /** - * Get the custom error message from translator. + * Get the custom error message from the translator. * * @param string $key * @return string @@ -336,7 +336,7 @@ public function getDisplayableValue($attribute, $value) return $value ? 'true' : 'false'; } - return $value; + return (string) $value; } /** diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index d645dbd6d5a6..d4a47af146c4 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -374,6 +374,46 @@ protected function replaceRequiredUnless($message, $attribute, $rule, $parameter return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); } + /** + * Replace all place-holders for the prohibited_if rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceProhibitedIf($message, $attribute, $rule, $parameters) + { + $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); + + $parameters[0] = $this->getDisplayableAttribute($parameters[0]); + + return str_replace([':other', ':value'], $parameters, $message); + } + + /** + * Replace all place-holders for the prohibited_unless rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceProhibitedUnless($message, $attribute, $rule, $parameters) + { + $other = $this->getDisplayableAttribute($parameters[0]); + + $values = []; + + foreach (array_slice($parameters, 1) as $value) { + $values[] = $this->getDisplayableValue($parameters[0], $value); + } + + return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); + } + /** * Replace all place-holders for the same rule. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 6bd27d55b07e..8f88aa2cd8df 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -556,7 +556,7 @@ public function validateDistinct($attribute, $value, $parameters) return empty(preg_grep('/^'.preg_quote($value, '/').'$/iu', $data)); } - return ! in_array($value, array_values($data)); + return ! in_array($value, array_values($data), in_array('strict', $parameters)); } /** @@ -617,15 +617,15 @@ public function validateEmail($attribute, $value, $parameters) ->unique() ->map(function ($validation) { if ($validation === 'rfc') { - return new RFCValidation(); + return new RFCValidation; } elseif ($validation === 'strict') { - return new NoRFCWarningsValidation(); + return new NoRFCWarningsValidation; } elseif ($validation === 'dns') { - return new DNSCheckValidation(); + return new DNSCheckValidation; } elseif ($validation === 'spoof') { - return new SpoofCheckValidation(); + return new SpoofCheckValidation; } elseif ($validation === 'filter') { - return new FilterEmailValidation(); + return new FilterEmailValidation; } elseif ($validation === 'filter_unicode') { return FilterEmailValidation::unicode(); } elseif (is_string($validation) && class_exists($validation)) { @@ -633,7 +633,7 @@ public function validateEmail($attribute, $value, $parameters) } }) ->values() - ->all() ?: [new RFCValidation()]; + ->all() ?: [new RFCValidation]; return (new EmailValidator)->isValid($value, new MultipleValidationWithAnd($validations)); } @@ -1063,7 +1063,7 @@ public function validateIn($attribute, $value, $parameters) } /** - * Validate that the values of an attribute is in another attribute. + * Validate that the values of an attribute are in another attribute. * * @param string $attribute * @param mixed $value @@ -1146,7 +1146,7 @@ public function validateJson($attribute, $value) return false; } - if (! is_scalar($value) && ! method_exists($value, '__toString')) { + if (! is_scalar($value) && ! is_null($value) && ! method_exists($value, '__toString')) { return false; } @@ -1425,15 +1425,70 @@ public function validateRequiredIf($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'required_if'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); - if (in_array($other, $values)) { + if (in_array($other, $values, is_bool($other))) { return $this->validateRequired($attribute, $value); } return true; } + /** + * Validate that an attribute does not exist. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibited($attribute, $value) + { + return false; + } + + /** + * Validate that an attribute does not exist when another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibitedIf($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'prohibited_if'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (in_array($other, $values, is_bool($other))) { + return ! $this->validateRequired($attribute, $value); + } + + return true; + } + + /** + * Validate that an attribute does not exist unless another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibitedUnless($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'prohibited_unless'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (! in_array($other, $values, is_bool($other))) { + return ! $this->validateRequired($attribute, $value); + } + + return true; + } + /** * Indicate that an attribute should be excluded when another attribute has a given value. * @@ -1446,9 +1501,9 @@ public function validateExcludeIf($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'exclude_if'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); - return ! in_array($other, $values); + return ! in_array($other, $values, is_bool($other)); } /** @@ -1463,9 +1518,9 @@ public function validateExcludeUnless($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'exclude_unless'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); - return in_array($other, $values); + return in_array($other, $values, is_bool($other)); } /** @@ -1493,7 +1548,7 @@ public function validateExcludeWithout($attribute, $value, $parameters) * @param array $parameters * @return array */ - protected function prepareValuesAndOther($parameters) + public function parseDependentRuleParameters($parameters) { $other = Arr::get($this->data, $parameters[0]); @@ -1552,9 +1607,9 @@ public function validateRequiredUnless($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'required_unless'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); - if (! in_array($other, $values)) { + if (! in_array($other, $values, is_bool($other))) { return $this->validateRequired($attribute, $value); } @@ -1579,7 +1634,7 @@ public function validateRequiredWith($attribute, $value, $parameters) } /** - * Validate that an attribute exists when all other attributes exists. + * Validate that an attribute exists when all other attributes exist. * * @param string $attribute * @param mixed $value @@ -1776,7 +1831,7 @@ public function validateUrl($attribute, $value) * (c) Fabien Potencier http://symfony.com */ $pattern = '~^ - (aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ark|attachment|aw|barion|beshare|bitcoin|bitcoincash|blob|bolo|browserext|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\+tcp|coap\+ws|coaps|coaps\+tcp|coaps\+ws|com-eventbrite-attendee|content|conti|crid|cvs|dab|data|dav|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|dpp|drm|drop|dtn|dvb|ed2k|elsi|example|facetime|fax|feed|feedready|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gizmoproject|go|gopher|graph|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|leaptofrogans|lorawan|lvlt|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|mongodb|moz|ms-access|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-lockscreencomponent-config|ms-media-stream-id|ms-mixedrealitycapture|ms-mobileplans|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|pack|palm|paparazzi|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|s3|secondlife|service|session|sftp|sgn|shttp|sieve|simpleledger|sip|sips|skype|smb|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|tg|things|thismessage|tip|tn3270|tool|turn|turns|tv|udp|unreal|urn|ut2004|v-event|vemmi|ventrilo|videotex|vnc|view-source|wais|webcal|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s):// # protocol + (aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ark|attachment|aw|barion|beshare|bitcoin|bitcoincash|blob|bolo|browserext|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\+tcp|coap\+ws|coaps|coaps\+tcp|coaps\+ws|com-eventbrite-attendee|content|conti|crid|cvs|dab|data|dav|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|dpp|drm|drop|dtn|dvb|ed2k|elsi|example|facetime|fax|feed|feedready|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gizmoproject|go|gopher|graph|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|leaptofrogans|lorawan|lvlt|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|mongodb|moz|ms-access|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-lockscreencomponent-config|ms-media-stream-id|ms-mixedrealitycapture|ms-mobileplans|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|pack|palm|paparazzi|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|s3|secondlife|service|session|sftp|sgn|shttp|sieve|simpleledger|sip|sips|skype|smb|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|tg|things|thismessage|tip|tn3270|tool|ts3server|turn|turns|tv|udp|unreal|urn|ut2004|v-event|vemmi|ventrilo|videotex|vnc|view-source|wais|webcal|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s):// # protocol (((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+)@)? # basic auth ( ([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name diff --git a/src/Illuminate/Validation/Factory.php b/src/Illuminate/Validation/Factory.php index cd2ff7066450..e9f75d738803 100755 --- a/src/Illuminate/Validation/Factory.php +++ b/src/Illuminate/Validation/Factory.php @@ -280,4 +280,27 @@ public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier) { $this->verifier = $presenceVerifier; } + + /** + * Get the container instance used by the validation factory. + * + * @return \Illuminate\Contracts\Container\Container + */ + public function getContainer() + { + return $this->container; + } + + /** + * Set the container instance used by the validation factory. + * + * @param \Illuminate\Contracts\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + + return $this; + } } diff --git a/src/Illuminate/Validation/ValidationData.php b/src/Illuminate/Validation/ValidationData.php index 74f552597c1c..86da0fd3a17a 100644 --- a/src/Illuminate/Validation/ValidationData.php +++ b/src/Illuminate/Validation/ValidationData.php @@ -8,7 +8,7 @@ class ValidationData { /** - * Initialize and gather data for given attribute. + * Initialize and gather data for the given attribute. * * @param string $attribute * @param array $masterData diff --git a/src/Illuminate/Validation/ValidationRuleParser.php b/src/Illuminate/Validation/ValidationRuleParser.php index 9438fb11f7b7..3fea4005aa28 100644 --- a/src/Illuminate/Validation/ValidationRuleParser.php +++ b/src/Illuminate/Validation/ValidationRuleParser.php @@ -186,57 +186,57 @@ protected function mergeRulesForAttribute($results, $attribute, $rules) /** * Extract the rule name and parameters from a rule. * - * @param array|string $rules + * @param array|string $rule * @return array */ - public static function parse($rules) + public static function parse($rule) { - if ($rules instanceof RuleContract) { - return [$rules, []]; + if ($rule instanceof RuleContract) { + return [$rule, []]; } - if (is_array($rules)) { - $rules = static::parseArrayRule($rules); + if (is_array($rule)) { + $rule = static::parseArrayRule($rule); } else { - $rules = static::parseStringRule($rules); + $rule = static::parseStringRule($rule); } - $rules[0] = static::normalizeRule($rules[0]); + $rule[0] = static::normalizeRule($rule[0]); - return $rules; + return $rule; } /** * Parse an array based rule. * - * @param array $rules + * @param array $rule * @return array */ - protected static function parseArrayRule(array $rules) + protected static function parseArrayRule(array $rule) { - return [Str::studly(trim(Arr::get($rules, 0))), array_slice($rules, 1)]; + return [Str::studly(trim(Arr::get($rule, 0))), array_slice($rule, 1)]; } /** * Parse a string based rule. * - * @param string $rules + * @param string $rule * @return array */ - protected static function parseStringRule($rules) + protected static function parseStringRule($rule) { $parameters = []; // The format for specifying validation rules and parameters follows an // easy {rule}:{parameters} formatting convention. For instance the // rule "Max:3" states that the value may only be three letters. - if (strpos($rules, ':') !== false) { - [$rules, $parameter] = explode(':', $rules, 2); + if (strpos($rule, ':') !== false) { + [$rule, $parameter] = explode(':', $rule, 2); - $parameters = static::parseParameters($rules, $parameter); + $parameters = static::parseParameters($rule, $parameter); } - return [Str::studly(trim($rules)), $parameters]; + return [Str::studly(trim($rule)), $parameters]; } /** diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 8cea4babb6db..2311b7357718 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -146,6 +146,13 @@ class Validator implements ValidatorContract */ public $customValues = []; + /** + * Indicates if the validator should stop on the first rule failure. + * + * @var bool + */ + protected $stopOnFirstFailure = false; + /** * All of the custom validator extensions. * @@ -220,6 +227,9 @@ class Validator implements ValidatorContract 'RequiredWithAll', 'RequiredWithout', 'RequiredWithoutAll', + 'Prohibited', + 'ProhibitedIf', + 'ProhibitedUnless', 'Same', 'Unique', ]; @@ -373,6 +383,10 @@ public function passes() continue; } + if ($this->stopOnFirstFailure && $this->messages->isNotEmpty()) { + break; + } + foreach ($rules as $rule) { $this->validateAttribute($attribute, $rule); @@ -515,7 +529,7 @@ protected function validateAttribute($attribute, $rule) [$rule, $parameters] = ValidationRuleParser::parse($rule); - if ($rule == '') { + if ($rule === '') { return; } @@ -1074,6 +1088,19 @@ public function sometimes($attribute, $rules, callable $callback) return $this; } + /** + * Instruct the validator to stop validating after the first rule failure. + * + * @param bool $stopOnFirstFailure + * @return $this + */ + public function stopOnFirstFailure($stopOnFirstFailure = true) + { + $this->stopOnFirstFailure = $stopOnFirstFailure; + + return $this; + } + /** * Register an array of custom validator extensions. * @@ -1230,7 +1257,7 @@ public function addCustomAttributes(array $customAttributes) } /** - * Set the callback that used to format an implicit attribute.. + * Set the callback that used to format an implicit attribute. * * @param callable|null $formatter * @return $this diff --git a/src/Illuminate/View/AnonymousComponent.php b/src/Illuminate/View/AnonymousComponent.php index a7887c5ad83f..2fb21e1afc4a 100644 --- a/src/Illuminate/View/AnonymousComponent.php +++ b/src/Illuminate/View/AnonymousComponent.php @@ -48,8 +48,13 @@ public function render() */ public function data() { - $this->attributes = $this->attributes ?: new ComponentAttributeBag; + $this->attributes = $this->attributes ?: $this->newAttributeBag(); - return $this->data + ['attributes' => $this->attributes]; + return array_merge( + optional($this->data['attributes'] ?? null)->getAttributes() ?: [], + $this->attributes->getAttributes(), + $this->data, + ['attributes' => $this->attributes] + ); } } diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 4482036072fd..14f8aefb4f49 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -100,7 +100,7 @@ class BladeCompiler extends Compiler implements CompilerInterface protected $echoFormat = 'e(%s)'; /** - * Array of footer lines to be added to template. + * Array of footer lines to be added to the template. * * @var array */ @@ -153,9 +153,11 @@ public function compile($path = null) $contents = $this->appendFilePath($contents); } - $this->files->put( - $this->getCompiledPath($this->getPath()), $contents + $this->ensureCompiledDirectoryExists( + $compiledPath = $this->getCompiledPath($this->getPath()) ); + + $this->files->put($compiledPath, $contents); } } @@ -251,7 +253,10 @@ public function compileString($value) $result = $this->addFooters($result); } - return $result; + return str_replace( + ['##BEGIN-COMPONENT-CLASS##', '##END-COMPONENT-CLASS##'], + '', + $result); } /** @@ -446,6 +451,8 @@ protected function compileStatement($match) */ protected function callCustomDirective($name, $value) { + $value = $value ?? ''; + if (Str::startsWith($value, '(') && Str::endsWith($value, ')')) { $value = Str::substr($value, 1, -1); } diff --git a/src/Illuminate/View/Compilers/Compiler.php b/src/Illuminate/View/Compilers/Compiler.php index 08648ad17b87..2a943e0f6309 100755 --- a/src/Illuminate/View/Compilers/Compiler.php +++ b/src/Illuminate/View/Compilers/Compiler.php @@ -71,4 +71,17 @@ public function isExpired($path) return $this->files->lastModified($path) >= $this->files->lastModified($compiled); } + + /** + * Create the compiled file directory if necessary. + * + * @param string $path + * @return void + */ + protected function ensureCompiledDirectoryExists($path) + { + if (! $this->files->exists(dirname($path))) { + $this->files->makeDirectory(dirname($path), 0777, true, true); + } + } } diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index b7b5300535b6..a69a704ec420 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -48,7 +48,7 @@ class ComponentTagCompiler protected $boundAttributes = []; /** - * Create new component tag compiler. + * Create a new component tag compiler. * * @param array $aliases * @param array $namespaces @@ -193,7 +193,7 @@ protected function compileSelfClosingTags(string $value) $attributes = $this->getAttributesFromAttributeString($matches['attributes']); - return $this->componentString($matches[1], $attributes)."\n@endcomponentClass "; + return $this->componentString($matches[1], $attributes)."\n@endComponentClass##END-COMPONENT-CLASS##"; }, $value); } @@ -230,7 +230,7 @@ protected function componentString(string $component, array $attributes) $parameters = $data->all(); } - return " @component('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).']) + return "##BEGIN-COMPONENT-CLASS##@component('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).']) withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; } @@ -384,7 +384,7 @@ public function partitionDataAndAttributes($class, array $attributes) */ protected function compileClosingTags(string $value) { - return preg_replace("/<\/\s*x[-\:][\w\-\:\.]*\s*>/", ' @endcomponentClass ', $value); + return preg_replace("/<\/\s*x[-\:][\w\-\:\.]*\s*>/", ' @endComponentClass##END-COMPONENT-CLASS##', $value); } /** @@ -460,12 +460,16 @@ protected function getAttributesFromAttributeString(string $attributeString) $value = "'".$this->compileAttributeEchos($value)."'"; } + if (Str::startsWith($attribute, '::')) { + $attribute = substr($attribute, 1); + } + return [$attribute => $value]; })->toArray(); } /** - * Parse the attribute bag in a given attribute string into it's fully-qualified syntax. + * Parse the attribute bag in a given attribute string into its fully-qualified syntax. * * @param string $attributeString * @return string @@ -490,7 +494,7 @@ protected function parseBindAttributes(string $attributeString) { $pattern = "/ (?:^|\s+) # start of the string or whitespace between attributes - : # attribute needs to start with a semicolon + :(?!:) # attribute needs to start with a single colon ([\w\-:.@]+) # match the actual attribute name = # only match attributes that have a value /xm"; diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php index 6f2169d69b89..6bae1e1cba4a 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php @@ -7,7 +7,7 @@ trait CompilesConditionals { /** - * Identifier for the first case in switch statement. + * Identifier for the first case in the switch statement. * * @var bool */ @@ -283,7 +283,7 @@ protected function compileEndSwitch() } /** - * Compile an once block into valid PHP. + * Compile a once block into valid PHP. * * @param string|null $id * @return string diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesInjections.php b/src/Illuminate/View/Compilers/Concerns/CompilesInjections.php index c295bcd448c5..a0d1ccf5ea31 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesInjections.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesInjections.php @@ -12,12 +12,12 @@ trait CompilesInjections */ protected function compileInject($expression) { - $segments = explode(',', preg_replace("/[\(\)\\\"\']/", '', $expression)); + $segments = explode(',', preg_replace("/[\(\)]/", '', $expression)); - $variable = trim($segments[0]); + $variable = trim($segments[0], " '\""); $service = trim($segments[1]); - return ""; + return ""; } } diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index fb1e448a1c3c..402a13abdc53 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -121,7 +121,7 @@ protected function createBladeViewFromString($factory, $contents) */ public function data() { - $this->attributes = $this->attributes ?: new ComponentAttributeBag; + $this->attributes = $this->attributes ?: $this->newAttributeBag(); return array_merge($this->extractPublicProperties(), $this->extractPublicMethods()); } @@ -266,13 +266,24 @@ public function withName($name) */ public function withAttributes(array $attributes) { - $this->attributes = $this->attributes ?: new ComponentAttributeBag; + $this->attributes = $this->attributes ?: $this->newAttributeBag(); $this->attributes->setAttributes($attributes); return $this; } + /** + * Get a new attribute bag instance. + * + * @param array $attributes + * @return \Illuminate\View\ComponentAttributeBag + */ + protected function newAttributeBag(array $attributes = []) + { + return new ComponentAttributeBag($attributes); + } + /** * Determine if the component should be rendered. * diff --git a/src/Illuminate/View/ComponentAttributeBag.php b/src/Illuminate/View/ComponentAttributeBag.php index 7a2b9f959ea6..e4a6eef709b8 100644 --- a/src/Illuminate/View/ComponentAttributeBag.php +++ b/src/Illuminate/View/ComponentAttributeBag.php @@ -70,7 +70,7 @@ public function has($key) /** * Only include the given attribute from the attribute array. * - * @param mixed|array $keys + * @param mixed $keys * @return static */ public function only($keys) @@ -173,6 +173,29 @@ public function exceptProps($keys) return $this->except($props); } + /** + * Conditionally merge classes into the attribute bag. + * + * @param mixed|array $classList + * @return static + */ + public function class($classList) + { + $classList = Arr::wrap($classList); + + $classes = []; + + foreach ($classList as $class => $constraint) { + if (is_numeric($class)) { + $classes[] = $constraint; + } elseif ($constraint) { + $classes[] = $class; + } + } + + return $this->merge(['class' => implode(' ', $classes)]); + } + /** * Merge additional attributes / values into the attribute bag. * diff --git a/src/Illuminate/View/Concerns/ManagesComponents.php b/src/Illuminate/View/Concerns/ManagesComponents.php index 574e91d6213b..81b2bdf6cbaf 100644 --- a/src/Illuminate/View/Concerns/ManagesComponents.php +++ b/src/Illuminate/View/Concerns/ManagesComponents.php @@ -4,9 +4,9 @@ use Closure; use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Contracts\View\View; use Illuminate\Support\Arr; use Illuminate\Support\HtmlString; -use Illuminate\View\View; use InvalidArgumentException; trait ManagesComponents diff --git a/src/Illuminate/View/Concerns/ManagesLayouts.php b/src/Illuminate/View/Concerns/ManagesLayouts.php index 785b9fa594de..d7d455933128 100644 --- a/src/Illuminate/View/Concerns/ManagesLayouts.php +++ b/src/Illuminate/View/Concerns/ManagesLayouts.php @@ -175,7 +175,7 @@ public static function parentPlaceholder($section = '') } /** - * Check if section exists. + * Check if the section exists. * * @param string $name * @return bool diff --git a/src/Illuminate/View/Engines/CompilerEngine.php b/src/Illuminate/View/Engines/CompilerEngine.php index d711fc670336..dca6a8710560 100755 --- a/src/Illuminate/View/Engines/CompilerEngine.php +++ b/src/Illuminate/View/Engines/CompilerEngine.php @@ -2,9 +2,9 @@ namespace Illuminate\View\Engines; -use ErrorException; use Illuminate\Filesystem\Filesystem; use Illuminate\View\Compilers\CompilerInterface; +use Illuminate\View\ViewException; use Throwable; class CompilerEngine extends PhpEngine @@ -76,7 +76,7 @@ public function get($path, array $data = []) */ protected function handleViewException(Throwable $e, $obLevel) { - $e = new ErrorException($this->getMessage($e), 0, 1, $e->getFile(), $e->getLine(), $e); + $e = new ViewException($this->getMessage($e), 0, 1, $e->getFile(), $e->getLine(), $e); parent::handleViewException($e, $obLevel); } diff --git a/src/Illuminate/View/ViewException.php b/src/Illuminate/View/ViewException.php new file mode 100644 index 000000000000..e6797a29a1e7 --- /dev/null +++ b/src/Illuminate/View/ViewException.php @@ -0,0 +1,41 @@ +getPrevious(); + + if (Reflector::isCallable($reportCallable = [$exception, 'report'])) { + return Container::getInstance()->call($reportCallable); + } + + return false; + } + + /** + * Render the exception into an HTTP response. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function render($request) + { + $exception = $this->getPrevious(); + + if ($exception && method_exists($exception, 'render')) { + return $exception->render($request); + } + } +} diff --git a/tests/Auth/AuthAccessGateTest.php b/tests/Auth/AuthAccessGateTest.php index 01bd87325450..c9c0d7d08ef2 100644 --- a/tests/Auth/AuthAccessGateTest.php +++ b/tests/Auth/AuthAccessGateTest.php @@ -184,10 +184,12 @@ public function testBeforeAndAfterCallbacksCanAllowGuests() $this->assertTrue($_SERVER['__laravel.gateAfter']); $this->assertFalse($_SERVER['__laravel.gateAfter2']); - unset($_SERVER['__laravel.gateBefore']); - unset($_SERVER['__laravel.gateBefore2']); - unset($_SERVER['__laravel.gateAfter']); - unset($_SERVER['__laravel.gateAfter2']); + unset( + $_SERVER['__laravel.gateBefore'], + $_SERVER['__laravel.gateBefore2'], + $_SERVER['__laravel.gateAfter'], + $_SERVER['__laravel.gateAfter2'] + ); } public function testResourceGatesCanBeDefined() @@ -262,9 +264,9 @@ public function testAfterCallbacksAreCalledWithResult() }); $gate->after(function ($user, $ability, $result) { - if ($ability == 'foo') { + if ($ability === 'foo') { $this->assertTrue($result, 'After callback on `foo` should receive true as result'); - } elseif ($ability == 'bar') { + } elseif ($ability === 'bar') { $this->assertFalse($result, 'After callback on `bar` should receive false as result'); } else { $this->assertNull($result, 'After callback on `missing` should receive null as result'); @@ -312,7 +314,7 @@ public function testAfterCallbacksDoNotOverrideEachOther() $gate = $this->getBasicGate(); $gate->after(function ($user, $ability, $result) { - return $ability == 'allow'; + return $ability === 'allow'; }); $gate->after(function ($user, $ability, $result) { diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php index 5d6e908f5d57..b07dbb2fcc6a 100644 --- a/tests/Broadcasting/UsePusherChannelsNamesTest.php +++ b/tests/Broadcasting/UsePusherChannelsNamesTest.php @@ -13,7 +13,7 @@ class UsePusherChannelsNamesTest extends TestCase */ public function testChannelNameNormalization($requestChannelName, $normalizedName) { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames(); + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; $this->assertSame( $normalizedName, @@ -23,7 +23,7 @@ public function testChannelNameNormalization($requestChannelName, $normalizedNam public function testChannelNameNormalizationSpecialCase() { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames(); + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; $this->assertSame( 'private-123', @@ -36,7 +36,7 @@ public function testChannelNameNormalizationSpecialCase() */ public function testIsGuardedChannel($requestChannelName, $_, $guarded) { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames(); + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; $this->assertSame( $guarded, diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 381200daa171..d502c04da2b3 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -14,6 +14,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\PostgresConnection; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\CallQueuedClosure; use Mockery as m; @@ -69,10 +70,7 @@ public function createSchema() */ protected function tearDown(): void { - unset($_SERVER['__finally.batch']); - unset($_SERVER['__then.batch']); - unset($_SERVER['__catch.batch']); - unset($_SERVER['__catch.exception']); + unset($_SERVER['__finally.batch'], $_SERVER['__then.batch'], $_SERVER['__catch.batch'], $_SERVER['__catch.exception']); $this->schema()->drop('job_batches'); @@ -112,7 +110,7 @@ public function test_jobs_can_be_added_to_the_batch() $this->assertEquals(3, $batch->totalJobs); $this->assertEquals(3, $batch->pendingJobs); - $this->assertTrue(is_string($job->batchId)); + $this->assertIsString($job->batchId); $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); } @@ -300,7 +298,7 @@ public function test_batch_state_can_be_inspected() $batch->cancelledAt = now(); $this->assertTrue($batch->cancelled()); - $this->assertTrue(is_string(json_encode($batch))); + $this->assertIsString(json_encode($batch)); } public function test_chain_can_be_added_to_batch() @@ -309,11 +307,11 @@ public function test_chain_can_be_added_to_batch() $batch = $this->createTestBatch($queue); - $chainHeadJob = new ChainHeadJob(); + $chainHeadJob = new ChainHeadJob; - $secondJob = new SecondTestJob(); + $secondJob = new SecondTestJob; - $thirdJob = new ThirdTestJob(); + $thirdJob = new ThirdTestJob; $queue->shouldReceive('connection')->once() ->with('test-connection') @@ -332,12 +330,79 @@ public function test_chain_can_be_added_to_batch() $this->assertEquals(3, $batch->totalJobs); $this->assertEquals(3, $batch->pendingJobs); - $this->assertTrue(is_string($chainHeadJob->batchId)); - $this->assertTrue(is_string($secondJob->batchId)); - $this->assertTrue(is_string($thirdJob->batchId)); + $this->assertSame('test-queue', $chainHeadJob->chainQueue); + $this->assertIsString($chainHeadJob->batchId); + $this->assertIsString($secondJob->batchId); + $this->assertIsString($thirdJob->batchId); $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); } + public function test_options_serialization_on_postgres() + { + $pendingBatch = (new PendingBatch(new Container, collect())) + ->onQueue('test-queue'); + + $connection = m::spy(PostgresConnection::class); + + $connection->shouldReceive('table')->andReturnSelf() + ->shouldReceive('where')->andReturnSelf(); + + $repository = new DatabaseBatchRepository( + new BatchFactory(m::mock(Factory::class)), $connection, 'job_batches' + ); + + $repository->store($pendingBatch); + + $connection->shouldHaveReceived('insert') + ->withArgs(function ($argument) use ($pendingBatch) { + return unserialize(base64_decode($argument['options'])) === $pendingBatch->options; + }); + } + + /** + * @dataProvider serializedOptions + */ + public function test_options_unserialize_on_postgres($serialize, $options) + { + $factory = m::mock(BatchFactory::class); + + $connection = m::spy(PostgresConnection::class); + + $connection->shouldReceive('table->where->first') + ->andReturn($m = (object) [ + 'id' => '', + 'name' => '', + 'total_jobs' => '', + 'pending_jobs' => '', + 'failed_jobs' => '', + 'failed_job_ids' => '[]', + 'options' => $serialize, + 'created_at' => null, + 'cancelled_at' => null, + 'finished_at' => null, + ]); + + $batch = (new DatabaseBatchRepository($factory, $connection, 'job_batches')); + + $factory->shouldReceive('make') + ->withSomeOfArgs($batch, '', '', '', '', '', '', $options); + + $batch->find(1); + } + + /** + * @return array + */ + public function serializedOptions() + { + $options = [1, 2]; + + return [ + [serialize($options), $options], + [base64_encode(serialize($options)), $options], + ]; + } + protected function createTestBatch($queue, $allowFailures = false) { $repository = new DatabaseBatchRepository(new BatchFactory($queue), DB::connection(), 'job_batches'); diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 3e1a2febbb82..b491ac47ef69 100755 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -5,6 +5,7 @@ use Illuminate\Cache\ArrayStore; use Illuminate\Support\Carbon; use PHPUnit\Framework\TestCase; +use stdClass; class CacheArrayStoreTest extends TestCase { @@ -198,7 +199,7 @@ public function testAnotherOwnerCanForceReleaseALock() public function testValuesAreNotStoredByReference() { $store = new ArrayStore($serialize = true); - $object = new \stdClass; + $object = new stdClass; $object->foo = true; $store->put('object', $object, 10); @@ -210,7 +211,7 @@ public function testValuesAreNotStoredByReference() public function testValuesAreStoredByReferenceIfSerializationIsDisabled() { $store = new ArrayStore; - $object = new \stdClass; + $object = new stdClass; $object->foo = true; $store->put('object', $object, 10); diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index 8a8d3446d173..dde234b01248 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -39,7 +39,7 @@ public function testForgetDriver() $cacheManager->shouldReceive('resolve') ->withArgs(['array']) ->times(4) - ->andReturn(new ArrayStore()); + ->andReturn(new ArrayStore); $cacheManager->shouldReceive('getDefaultDriver') ->once() @@ -64,7 +64,7 @@ public function testForgetDriverForgets() ], ]); $cacheManager->extend('forget', function () { - return new ArrayStore(); + return new ArrayStore; }); $cacheManager->store('forget')->forever('foo', 'bar'); diff --git a/tests/Cache/CacheMemcachedStoreTest.php b/tests/Cache/CacheMemcachedStoreTest.php index 5b29dd70b279..b1aad12ad29d 100755 --- a/tests/Cache/CacheMemcachedStoreTest.php +++ b/tests/Cache/CacheMemcachedStoreTest.php @@ -11,7 +11,7 @@ class CacheMemcachedStoreTest extends TestCase { - public function tearDown(): void + protected function tearDown(): void { m::close(); diff --git a/tests/Console/CommandTest.php b/tests/Console/CommandTest.php index cc7a7403a9f8..e7f8d76ee858 100644 --- a/tests/Console/CommandTest.php +++ b/tests/Console/CommandTest.php @@ -33,7 +33,7 @@ public function handle() $command->setLaravel($application); $input = new ArrayInput([]); - $output = new NullOutput(); + $output = new NullOutput; $application->shouldReceive('make')->with(OutputStyle::class, ['input' => $input, 'output' => $output])->andReturn(m::mock(OutputStyle::class)); $application->shouldReceive('call')->with([$command, 'handle'])->andReturnUsing(function () use ($command, $application) { @@ -84,7 +84,7 @@ protected function getOptions() '--option-one' => 'test-first-option', '--option-two' => 'test-second-option', ]); - $output = new NullOutput(); + $output = new NullOutput; $command->run($input, $output); diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index a5b05a9dd787..20d8f8ff92ba 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -66,7 +66,7 @@ public function testBuildCommandInBackgroundUsingWindows() public function testBuildCommandSendOutputTo() { - $quote = (DIRECTORY_SEPARATOR == '\\') ? '"' : "'"; + $quote = (DIRECTORY_SEPARATOR === '\\') ? '"' : "'"; $event = new Event(m::mock(EventMutex::class), 'php -i'); @@ -81,7 +81,7 @@ public function testBuildCommandSendOutputTo() public function testBuildCommandAppendOutput() { - $quote = (DIRECTORY_SEPARATOR == '\\') ? '"' : "'"; + $quote = (DIRECTORY_SEPARATOR === '\\') ? '"' : "'"; $event = new Event(m::mock(EventMutex::class), 'php -i'); diff --git a/tests/Container/ContainerResolveNonInstantiableTest.php b/tests/Container/ContainerResolveNonInstantiableTest.php new file mode 100644 index 000000000000..1f39322c40b8 --- /dev/null +++ b/tests/Container/ContainerResolveNonInstantiableTest.php @@ -0,0 +1,75 @@ +make(ParentClass::class, ['i' => 42]); + + $this->assertSame(42, $object->i); + } + + public function testResolvingNonInstantiableWithVariadicRemovesWiths() + { + $container = new Container; + $parent = $container->make(VariadicParentClass::class, ['i' => 42]); + + $this->assertCount(0, $parent->child->objects); + $this->assertSame(42, $parent->i); + } +} + +interface TestInterface +{ +} + +class ParentClass +{ + /** + * @var int + */ + public $i; + + public function __construct(TestInterface $testObject = null, int $i = 0) + { + $this->i = $i; + } +} + +class VariadicParentClass +{ + /** + * @var \Illuminate\Tests\Container\ChildClass + */ + public $child; + + /** + * @var int + */ + public $i; + + public function __construct(ChildClass $child, int $i = 0) + { + $this->child = $child; + $this->i = $i; + } +} + +class ChildClass +{ + /** + * @var array + */ + public $objects; + + public function __construct(TestInterface ...$objects) + { + $this->objects = $objects; + } +} diff --git a/tests/Container/ContainerTaggingTest.php b/tests/Container/ContainerTaggingTest.php index 754c977e3c12..5cbc8ea57d91 100644 --- a/tests/Container/ContainerTaggingTest.php +++ b/tests/Container/ContainerTaggingTest.php @@ -48,7 +48,7 @@ public function testContainerTags() public function testTaggedServicesAreLazyLoaded() { $container = $this->createPartialMock(Container::class, ['make']); - $container->expects($this->once())->method('make')->willReturn(new ContainerImplementationTaggedStub()); + $container->expects($this->once())->method('make')->willReturn(new ContainerImplementationTaggedStub); $container->tag(ContainerImplementationTaggedStub::class, ['foo']); $container->tag(ContainerImplementationTaggedStubTwo::class, ['foo']); diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 8f4b01a2bfd5..5cdb0204ddd9 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -5,9 +5,11 @@ use Illuminate\Container\Container; use Illuminate\Container\EntryNotFoundException; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Container\CircularDependencyException; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use stdClass; +use TypeError; class ContainerTest extends TestCase { @@ -122,10 +124,10 @@ public function testSharedConcreteResolution() public function testBindFailsLoudlyWithInvalidArgument() { - $this->expectException(\TypeError::class); + $this->expectException(TypeError::class); $container = new Container; - $concrete = new ContainerConcreteStub(); + $concrete = new ContainerConcreteStub; $container->bind(ContainerConcreteStub::class, $concrete); } @@ -562,6 +564,38 @@ public function testContainerCanResolveClasses() $this->assertInstanceOf(ContainerConcreteStub::class, $class); } + + // public function testContainerCanCatchCircularDependency() + // { + // $this->expectException(CircularDependencyException::class); + + // $container = new Container; + // $container->get(CircularAStub::class); + // } +} + +class CircularAStub +{ + public function __construct(CircularBStub $b) + { + // + } +} + +class CircularBStub +{ + public function __construct(CircularCStub $c) + { + // + } +} + +class CircularCStub +{ + public function __construct(CircularAStub $a) + { + // + } } class ContainerConcreteStub diff --git a/tests/Container/ContextualBindingTest.php b/tests/Container/ContextualBindingTest.php index 17a7d73f00ea..026a22f2ab82 100644 --- a/tests/Container/ContextualBindingTest.php +++ b/tests/Container/ContextualBindingTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Container; +use Illuminate\Config\Repository; use Illuminate\Container\Container; use PHPUnit\Framework\TestCase; @@ -359,6 +360,127 @@ public function testContextualBindingGivesTagsForVariadic() $this->assertInstanceOf(ContainerContextImplementationStub::class, $resolvedInstance->stubs[0]); $this->assertInstanceOf(ContainerContextImplementationStubTwo::class, $resolvedInstance->stubs[1]); } + + public function testContextualBindingGivesValuesFromConfigOptionalValueNull() + { + $container = new Container; + + $container->singleton('config', function () { + return new Repository([ + 'test' => [ + 'username' => 'laravel', + 'password' => 'hunter42', + ], + ]); + }); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$username') + ->giveConfig('test.username'); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$password') + ->giveConfig('test.password'); + + $resolvedInstance = $container->make(ContainerTestContextInjectFromConfigIndividualValues::class); + + $this->assertSame('laravel', $resolvedInstance->username); + $this->assertSame('hunter42', $resolvedInstance->password); + $this->assertNull($resolvedInstance->alias); + } + + public function testContextualBindingGivesValuesFromConfigOptionalValueSet() + { + $container = new Container; + + $container->singleton('config', function () { + return new Repository([ + 'test' => [ + 'username' => 'laravel', + 'password' => 'hunter42', + 'alias' => 'lumen', + ], + ]); + }); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$username') + ->giveConfig('test.username'); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$password') + ->giveConfig('test.password'); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$alias') + ->giveConfig('test.alias'); + + $resolvedInstance = $container->make(ContainerTestContextInjectFromConfigIndividualValues::class); + + $this->assertSame('laravel', $resolvedInstance->username); + $this->assertSame('hunter42', $resolvedInstance->password); + $this->assertSame('lumen', $resolvedInstance->alias); + } + + public function testContextualBindingGivesValuesFromConfigWithDefault() + { + $container = new Container; + + $container->singleton('config', function () { + return new Repository([ + 'test' => [ + 'password' => 'hunter42', + ], + ]); + }); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$username') + ->giveConfig('test.username', 'DEFAULT_USERNAME'); + + $container + ->when(ContainerTestContextInjectFromConfigIndividualValues::class) + ->needs('$password') + ->giveConfig('test.password'); + + $resolvedInstance = $container->make(ContainerTestContextInjectFromConfigIndividualValues::class); + + $this->assertSame('DEFAULT_USERNAME', $resolvedInstance->username); + $this->assertSame('hunter42', $resolvedInstance->password); + $this->assertNull($resolvedInstance->alias); + } + + public function testContextualBindingGivesValuesFromConfigArray() + { + $container = new Container; + + $container->singleton('config', function () { + return new Repository([ + 'test' => [ + 'username' => 'laravel', + 'password' => 'hunter42', + 'alias' => 'lumen', + ], + ]); + }); + + $container + ->when(ContainerTestContextInjectFromConfigArray::class) + ->needs('$settings') + ->giveConfig('test'); + + $resolvedInstance = $container->make(ContainerTestContextInjectFromConfigArray::class); + + $this->assertSame('laravel', $resolvedInstance->settings['username']); + $this->assertSame('hunter42', $resolvedInstance->settings['password']); + $this->assertSame('lumen', $resolvedInstance->settings['alias']); + } } interface IContainerContextContractStub @@ -474,3 +596,27 @@ public function __construct(ContainerContextNonContractStub $other, IContainerCo $this->stubs = $stubs; } } + +class ContainerTestContextInjectFromConfigIndividualValues +{ + public $username; + public $password; + public $alias = null; + + public function __construct($username, $password, $alias = null) + { + $this->username = $username; + $this->password = $password; + $this->alias = $alias; + } +} + +class ContainerTestContextInjectFromConfigArray +{ + public $settings; + + public function __construct($settings) + { + $this->settings = $settings; + } +} diff --git a/tests/Container/ResolvingCallbackTest.php b/tests/Container/ResolvingCallbackTest.php index e8aeecb73a68..91a749710388 100644 --- a/tests/Container/ResolvingCallbackTest.php +++ b/tests/Container/ResolvingCallbackTest.php @@ -303,7 +303,7 @@ public function testResolvingCallbacksAreCallWhenRebindHappenForResolvedAbstract $this->assertEquals(3, $callCounter); $container->bind(ResolvingContractStub::class, function () { - return new ResolvingImplementationStubTwo(); + return new ResolvingImplementationStubTwo; }); $this->assertEquals(4, $callCounter); diff --git a/tests/Cookie/Middleware/AddQueuedCookiesToResponseTest.php b/tests/Cookie/Middleware/AddQueuedCookiesToResponseTest.php index 335fa4e9beab..48447c2d3501 100644 --- a/tests/Cookie/Middleware/AddQueuedCookiesToResponseTest.php +++ b/tests/Cookie/Middleware/AddQueuedCookiesToResponseTest.php @@ -13,14 +13,14 @@ class AddQueuedCookiesToResponseTest extends TestCase { public function testHandle(): void { - $cookieJar = new CookieJar(); + $cookieJar = new CookieJar; $cookieOne = $cookieJar->make('foo', 'bar', 0, '/path'); $cookieTwo = $cookieJar->make('foo', 'rab', 0, '/'); $cookieJar->queue($cookieOne); $cookieJar->queue($cookieTwo); $addQueueCookiesToResponseMiddleware = new AddQueuedCookiesToResponse($cookieJar); $next = function (Request $request) { - return new Response(); + return new Response; }; $this->assertEquals( [ @@ -33,7 +33,7 @@ public function testHandle(): void ], ], ], - $addQueueCookiesToResponseMiddleware->handle(new Request(), $next)->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY) + $addQueueCookiesToResponseMiddleware->handle(new Request, $next)->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY) ); } } diff --git a/tests/Database/DatabaseAbstractSchemaGrammarTest.php b/tests/Database/DatabaseAbstractSchemaGrammarTest.php new file mode 100755 index 000000000000..04e23eb264be --- /dev/null +++ b/tests/Database/DatabaseAbstractSchemaGrammarTest.php @@ -0,0 +1,37 @@ +expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support creating databases.'); + + $grammar->compileCreateDatabase('foo', m::mock(Connection::class)); + } + + public function testDropDatabaseIfExists() + { + $grammar = new class extends Grammar {}; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support dropping databases.'); + + $grammar->compileDropDatabaseIfExists('foo'); + } +} diff --git a/tests/Database/DatabaseEloquentBelongsToManyLazyByIdTest.php b/tests/Database/DatabaseEloquentBelongsToManyLazyByIdTest.php new file mode 100644 index 000000000000..cbdf1ffbda19 --- /dev/null +++ b/tests/Database/DatabaseEloquentBelongsToManyLazyByIdTest.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('aid'); + $table->string('title'); + }); + + $this->schema()->create('article_user', function ($table) { + $table->integer('article_id')->unsigned(); + $table->foreign('article_id')->references('aid')->on('articles'); + $table->integer('user_id')->unsigned(); + $table->foreign('user_id')->references('id')->on('users'); + }); + } + + public function testBelongsToLazyById() + { + $this->seedData(); + + $user = BelongsToManyLazyByIdTestTestUser::query()->first(); + $i = 0; + + $user->articles()->lazyById(1)->each(function ($model) use (&$i) { + $i++; + $this->assertEquals($i, $model->aid); + }); + + $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 = BelongsToManyLazyByIdTestTestUser::create(['id' => 1, 'email' => 'taylorotwell@gmail.com']); + BelongsToManyLazyByIdTestTestArticle::query()->insert([ + ['aid' => 1, 'title' => 'Another title'], + ['aid' => 2, 'title' => 'Another title'], + ['aid' => 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 BelongsToManyLazyByIdTestTestUser extends Eloquent +{ + protected $table = 'users'; + protected $fillable = ['id', 'email']; + public $timestamps = false; + + public function articles() + { + return $this->belongsToMany(BelongsToManyLazyByIdTestTestArticle::class, 'article_user', 'user_id', 'article_id'); + } +} + +class BelongsToManyLazyByIdTestTestArticle extends Eloquent +{ + protected $primaryKey = 'aid'; + protected $table = 'articles'; + protected $keyType = 'string'; + public $incrementing = false; + public $timestamps = false; + protected $fillable = ['aid', 'title']; +} diff --git a/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php b/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php new file mode 100644 index 000000000000..458a4d6dd6a3 --- /dev/null +++ b/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php @@ -0,0 +1,77 @@ +getRelation(); + $model1 = m::mock(Model::class); + $model1->shouldReceive('getAttribute')->with('parent_key')->andReturn(1); + $model1->shouldReceive('getAttribute')->with('foo')->passthru(); + $model1->shouldReceive('hasGetMutator')->andReturn(false); + $model1->shouldReceive('getCasts')->andReturn([]); + $model1->shouldReceive('getRelationValue', 'relationLoaded', 'setRelation')->passthru(); + + $model2 = m::mock(Model::class); + $model2->shouldReceive('getAttribute')->with('parent_key')->andReturn(2); + $model2->shouldReceive('getAttribute')->with('foo')->passthru(); + $model2->shouldReceive('hasGetMutator')->andReturn(false); + $model2->shouldReceive('getCasts')->andReturn([]); + $model2->shouldReceive('getRelationValue', 'relationLoaded', 'setRelation')->passthru(); + + $result1 = (object) [ + 'pivot' => (object) [ + 'foreign_key' => new class { + public function __toString() + { + return '1'; + } + }, + ], + ]; + + $models = $relation->match([$model1, $model2], Collection::wrap($result1), 'foo'); + self::assertNull($models[1]->foo); + self::assertEquals(1, $models[0]->foo->count()); + self::assertContains($result1, $models[0]->foo); + } + + protected function getRelation() + { + $builder = m::mock(Builder::class); + $related = m::mock(Model::class); + $related->shouldReceive('newCollection')->passthru(); + $builder->shouldReceive('getModel')->andReturn($related); + $related->shouldReceive('qualifyColumn'); + $builder->shouldReceive('join', 'where'); + + return new BelongsToMany( + $builder, + new EloquentBelongsToManyModelStub, + 'relation', + 'foreign_key', + 'id', + 'parent_key', + 'related_key' + ); + } +} + +class EloquentBelongsToManyModelStub extends Model +{ + public $foreign_key = 'foreign.value'; +} diff --git a/tests/Database/DatabaseEloquentBelongsToTest.php b/tests/Database/DatabaseEloquentBelongsToTest.php index 1d77ba37c108..b8c00b572a5d 100755 --- a/tests/Database/DatabaseEloquentBelongsToTest.php +++ b/tests/Database/DatabaseEloquentBelongsToTest.php @@ -109,14 +109,29 @@ public function testModelsAreProperlyMatchedToParents() $result1->shouldReceive('getAttribute')->with('id')->andReturn(1); $result2 = m::mock(stdClass::class); $result2->shouldReceive('getAttribute')->with('id')->andReturn(2); + $result3 = m::mock(stdClass::class); + $result3->shouldReceive('getAttribute')->with('id')->andReturn(new class { + public function __toString() + { + return '3'; + } + }); $model1 = new EloquentBelongsToModelStub; $model1->foreign_key = 1; $model2 = new EloquentBelongsToModelStub; $model2->foreign_key = 2; - $models = $relation->match([$model1, $model2], new Collection([$result1, $result2]), 'foo'); + $model3 = new EloquentBelongsToModelStub; + $model3->foreign_key = new class { + public function __toString() + { + return '3'; + } + }; + $models = $relation->match([$model1, $model2, $model3], new Collection([$result1, $result2, $result3]), 'foo'); $this->assertEquals(1, $models[0]->foo->getAttribute('id')); $this->assertEquals(2, $models[1]->foo->getAttribute('id')); + $this->assertEquals('3', $models[2]->foo->getAttribute('id')); } public function testAssociateMethodSetsForeignKeyOnModel() diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 6e0a59148954..0809f8e0fe98 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -382,6 +382,118 @@ public function testChunkPaginatesUsingIdWithCountZero() }, 'someIdField'); } + public function testLazyWithLastChunkComplete() + { + $builder = m::mock(Builder::class.'[forPage,get]', [$this->getMockQueryBuilder()]); + $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + + $builder->shouldReceive('forPage')->once()->with(1, 2)->andReturnSelf(); + $builder->shouldReceive('forPage')->once()->with(2, 2)->andReturnSelf(); + $builder->shouldReceive('forPage')->once()->with(3, 2)->andReturnSelf(); + $builder->shouldReceive('get')->times(3)->andReturn( + new Collection(['foo1', 'foo2']), + new Collection(['foo3', 'foo4']), + new Collection([]) + ); + + $this->assertEquals( + ['foo1', 'foo2', 'foo3', 'foo4'], + $builder->lazy(2)->all() + ); + } + + public function testLazyWithLastChunkPartial() + { + $builder = m::mock(Builder::class.'[forPage,get]', [$this->getMockQueryBuilder()]); + $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + + $builder->shouldReceive('forPage')->once()->with(1, 2)->andReturnSelf(); + $builder->shouldReceive('forPage')->once()->with(2, 2)->andReturnSelf(); + $builder->shouldReceive('get')->times(2)->andReturn( + new Collection(['foo1', 'foo2']), + new Collection(['foo3']) + ); + + $this->assertEquals( + ['foo1', 'foo2', 'foo3'], + $builder->lazy(2)->all() + ); + } + + public function testLazyIsLazy() + { + $builder = m::mock(Builder::class.'[forPage,get]', [$this->getMockQueryBuilder()]); + $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + + $builder->shouldReceive('forPage')->once()->with(1, 2)->andReturnSelf(); + $builder->shouldReceive('get')->once()->andReturn(new Collection(['foo1', 'foo2'])); + + $this->assertEquals(['foo1', 'foo2'], $builder->lazy(2)->take(2)->all()); + } + + public function testLazyByIdWithLastChunkComplete() + { + $builder = m::mock(Builder::class.'[forPageAfterId,get]', [$this->getMockQueryBuilder()]); + $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + + $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); + $chunk2 = new Collection([(object) ['someIdField' => 10], (object) ['someIdField' => 11]]); + $chunk3 = new Collection([]); + $builder->shouldReceive('forPageAfterId')->once()->with(2, 0, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('forPageAfterId')->once()->with(2, 2, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('forPageAfterId')->once()->with(2, 11, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); + + $this->assertEquals( + [ + (object) ['someIdField' => 1], + (object) ['someIdField' => 2], + (object) ['someIdField' => 10], + (object) ['someIdField' => 11], + ], + $builder->lazyById(2, 'someIdField')->all() + ); + } + + public function testLazyByIdWithLastChunkPartial() + { + $builder = m::mock(Builder::class.'[forPageAfterId,get]', [$this->getMockQueryBuilder()]); + $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + + $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); + $chunk2 = new Collection([(object) ['someIdField' => 10]]); + $builder->shouldReceive('forPageAfterId')->once()->with(2, 0, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('forPageAfterId')->once()->with(2, 2, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('get')->times(2)->andReturn($chunk1, $chunk2); + + $this->assertEquals( + [ + (object) ['someIdField' => 1], + (object) ['someIdField' => 2], + (object) ['someIdField' => 10], + ], + $builder->lazyById(2, 'someIdField')->all() + ); + } + + public function testLazyByIdIsLazy() + { + $builder = m::mock(Builder::class.'[forPageAfterId,get]', [$this->getMockQueryBuilder()]); + $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + + $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); + $builder->shouldReceive('forPageAfterId')->once()->with(2, 0, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('get')->once()->andReturn($chunk1); + + $this->assertEquals( + [ + (object) ['someIdField' => 1], + (object) ['someIdField' => 2], + ], + $builder->lazyById(2, 'someIdField')->take(2)->all() + ); + } + public function testPluckReturnsTheMutatedAttributesOfAModel() { $builder = $this->getBuilder(); @@ -467,7 +579,7 @@ public function testGlobalMacrosAreCalledOnBuilder() $builder = $this->getBuilder(); $this->assertTrue(Builder::hasGlobalMacro('foo')); - $this->assertEquals('bar', $builder->foo('bar')); + $this->assertSame('bar', $builder->foo('bar')); $this->assertEquals($builder->bam(), $builder->getQuery()); } @@ -660,7 +772,7 @@ public function testQueryPassThru() $this->assertSame('foo', $builder->raw('bar')); $builder = $this->getBuilder(); - $grammar = new Grammar(); + $grammar = new Grammar; $builder->getQuery()->shouldReceive('getGrammar')->once()->andReturn($grammar); $this->assertSame($grammar, $builder->getGrammar()); } @@ -1097,7 +1209,7 @@ public function testWhereKeyMethodWithInt() public function testWhereKeyMethodWithStringZero() { - $model = new EloquentBuilderTestStubStringPrimaryKey(); + $model = new EloquentBuilderTestStubStringPrimaryKey; $builder = $this->getBuilder()->setModel($model); $keyName = $model->getQualifiedKeyName(); @@ -1110,7 +1222,7 @@ public function testWhereKeyMethodWithStringZero() public function testWhereKeyMethodWithStringNull() { - $model = new EloquentBuilderTestStubStringPrimaryKey(); + $model = new EloquentBuilderTestStubStringPrimaryKey; $builder = $this->getBuilder()->setModel($model); $keyName = $model->getQualifiedKeyName(); @@ -1149,7 +1261,7 @@ public function testWhereKeyMethodWithCollection() public function testWhereKeyNotMethodWithStringZero() { - $model = new EloquentBuilderTestStubStringPrimaryKey(); + $model = new EloquentBuilderTestStubStringPrimaryKey; $builder = $this->getBuilder()->setModel($model); $keyName = $model->getQualifiedKeyName(); @@ -1162,7 +1274,7 @@ public function testWhereKeyNotMethodWithStringZero() public function testWhereKeyNotMethodWithStringNull() { - $model = new EloquentBuilderTestStubStringPrimaryKey(); + $model = new EloquentBuilderTestStubStringPrimaryKey; $builder = $this->getBuilder()->setModel($model); $keyName = $model->getQualifiedKeyName(); diff --git a/tests/Database/DatabaseEloquentCollectionTest.php b/tests/Database/DatabaseEloquentCollectionTest.php index 0f2ffa48490b..34df7c07672d 100755 --- a/tests/Database/DatabaseEloquentCollectionTest.php +++ b/tests/Database/DatabaseEloquentCollectionTest.php @@ -278,10 +278,10 @@ public function testCollectionDiffsWithGivenCollection() public function testCollectionReturnsDuplicateBasedOnlyOnKeys() { - $one = new TestEloquentCollectionModel(); - $two = new TestEloquentCollectionModel(); - $three = new TestEloquentCollectionModel(); - $four = new TestEloquentCollectionModel(); + $one = new TestEloquentCollectionModel; + $two = new TestEloquentCollectionModel; + $three = new TestEloquentCollectionModel; + $four = new TestEloquentCollectionModel; $one->id = 1; $one->someAttribute = '1'; $two->id = 1; @@ -346,10 +346,10 @@ public function testCollectionReturnsUniqueItems() public function testCollectionReturnsUniqueStrictBasedOnKeysOnly() { - $one = new TestEloquentCollectionModel(); - $two = new TestEloquentCollectionModel(); - $three = new TestEloquentCollectionModel(); - $four = new TestEloquentCollectionModel(); + $one = new TestEloquentCollectionModel; + $two = new TestEloquentCollectionModel; + $three = new TestEloquentCollectionModel; + $four = new TestEloquentCollectionModel; $one->id = 1; $one->someAttribute = '1'; $two->id = 1; diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index 7f0935f39e27..2d1def239514 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -2,14 +2,15 @@ namespace Illuminate\Tests\Database; +use Faker\Generator; use Illuminate\Container\Container; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Database\Eloquent\Model as Eloquent; -use Illuminate\Foundation\Application; use Mockery; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ class DatabaseEloquentFactoryTest extends TestCase protected function setUp(): void { $container = Container::getInstance(); - $container->singleton(\Faker\Generator::class, function ($app, $parameters) { + $container->singleton(Generator::class, function ($app, $parameters) { return \Faker\Factory::create('en_US'); }); $container->instance(Application::class, $app = Mockery::mock(Application::class)); @@ -199,8 +200,7 @@ public function test_after_creating_and_making_callbacks_are_called() $this->assertSame($user, $_SERVER['__test.user.making']); $this->assertSame($user, $_SERVER['__test.user.creating']); - unset($_SERVER['__test.user.making']); - unset($_SERVER['__test.user.creating']); + unset($_SERVER['__test.user.making'], $_SERVER['__test.user.creating']); } public function test_has_many_relationship() @@ -231,9 +231,7 @@ public function test_has_many_relationship() $this->assertInstanceOf(Eloquent::class, $_SERVER['__test.post.creating-user']); $this->assertInstanceOf(Eloquent::class, $_SERVER['__test.post.state-user']); - unset($_SERVER['__test.post.creating-post']); - unset($_SERVER['__test.post.creating-user']); - unset($_SERVER['__test.post.state-user']); + unset($_SERVER['__test.post.creating-post'], $_SERVER['__test.post.creating-user'], $_SERVER['__test.post.state-user']); } public function test_belongs_to_relationship() @@ -243,7 +241,7 @@ public function test_belongs_to_relationship() ->create(); $this->assertCount(3, $posts->filter(function ($post) { - return $post->user->name == 'Taylor Otwell'; + return $post->user->name === 'Taylor Otwell'; })); $this->assertCount(1, FactoryTestUser::all()); @@ -330,8 +328,7 @@ public function test_belongs_to_many_relationship() $this->assertInstanceOf(Eloquent::class, $_SERVER['__test.role.creating-role']); $this->assertInstanceOf(Eloquent::class, $_SERVER['__test.role.creating-user']); - unset($_SERVER['__test.role.creating-role']); - unset($_SERVER['__test.role.creating-user']); + unset($_SERVER['__test.role.creating-role'], $_SERVER['__test.role.creating-user']); } public function test_belongs_to_many_relationship_with_existing_model_instances() @@ -401,11 +398,11 @@ public function test_sequences() $this->assertCount(4, $user->roles); $this->assertCount(2, $user->roles->filter(function ($role) { - return $role->pivot->admin == 'Y'; + return $role->pivot->admin === 'Y'; })); $this->assertCount(2, $user->roles->filter(function ($role) { - return $role->pivot->admin == 'N'; + return $role->pivot->admin === 'N'; })); } @@ -473,6 +470,16 @@ public function test_dynamic_has_and_for_methods() $this->assertCount(2, $post->comments); } + public function test_can_be_macroable() + { + $factory = FactoryTestUserFactory::new(); + $factory->macro('getFoo', function () { + return 'Hello World'; + }); + + $this->assertSame('Hello World', $factory->getFoo()); + } + /** * Get a database connection instance. * diff --git a/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php b/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php index d1572adcaade..b4c207efa820 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php @@ -321,6 +321,52 @@ public function testEachReturnsCorrectModels() }); } + public function testLazyReturnsCorrectModels() + { + $this->seedData(); + $this->seedDataExtended(); + $country = HasManyThroughTestCountry::find(2); + + $country->posts()->lazy(10)->each(function ($post) { + $this->assertEquals([ + 'id', + 'user_id', + 'title', + 'body', + 'email', + 'created_at', + 'updated_at', + 'laravel_through_key', + ], array_keys($post->getAttributes())); + }); + } + + public function testLazyById() + { + $this->seedData(); + $this->seedDataExtended(); + $country = HasManyThroughTestCountry::find(2); + + $i = 0; + + $country->posts()->lazyById(2)->each(function ($post) use (&$i, &$count) { + $i++; + + $this->assertEquals([ + 'id', + 'user_id', + 'title', + 'body', + 'email', + 'created_at', + 'updated_at', + 'laravel_through_key', + ], array_keys($post->getAttributes())); + }); + + $this->assertEquals(6, $i); + } + public function testIntermediateSoftDeletesAreIgnored() { $this->seedData(); diff --git a/tests/Database/DatabaseEloquentHasOneTest.php b/tests/Database/DatabaseEloquentHasOneTest.php index 0e35d679784d..cac4c84ca53b 100755 --- a/tests/Database/DatabaseEloquentHasOneTest.php +++ b/tests/Database/DatabaseEloquentHasOneTest.php @@ -160,6 +160,13 @@ public function testModelsAreProperlyMatchedToParents() $result1->foreign_key = 1; $result2 = new EloquentHasOneModelStub; $result2->foreign_key = 2; + $result3 = new EloquentHasOneModelStub; + $result3->foreign_key = new class { + public function __toString() + { + return '4'; + } + }; $model1 = new EloquentHasOneModelStub; $model1->id = 1; @@ -167,12 +174,15 @@ public function testModelsAreProperlyMatchedToParents() $model2->id = 2; $model3 = new EloquentHasOneModelStub; $model3->id = 3; + $model4 = new EloquentHasOneModelStub; + $model4->id = 4; - $models = $relation->match([$model1, $model2, $model3], new Collection([$result1, $result2]), 'foo'); + $models = $relation->match([$model1, $model2, $model3, $model4], new Collection([$result1, $result2, $result3]), 'foo'); $this->assertEquals(1, $models[0]->foo->foreign_key); $this->assertEquals(2, $models[1]->foo->foreign_key); $this->assertNull($models[2]->foo); + $this->assertEquals('4', $models[3]->foo->foreign_key); } public function testRelationCountQueryCanBeBuilt() diff --git a/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php b/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php index 67b9824f98e3..0b2bf287bcdb 100644 --- a/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php +++ b/tests/Database/DatabaseEloquentHasOneThroughIntegrationTest.php @@ -234,6 +234,25 @@ public function testEachReturnsCorrectModels() }); } + public function testLazyReturnsCorrectModels() + { + $this->seedData(); + $this->seedDataExtended(); + $position = HasOneThroughTestPosition::find(1); + + $position->contract()->lazy()->each(function ($contract) { + $this->assertEquals([ + 'id', + 'user_id', + 'title', + 'body', + 'email', + 'created_at', + 'updated_at', + 'laravel_through_key', ], array_keys($contract->getAttributes())); + }); + } + public function testIntermediateSoftDeletesAreIgnored() { $this->seedData(); diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index c3b33a9d941b..4c8f77398733 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -805,6 +805,27 @@ public function testHasOnMorphToRelationship() $this->assertEquals(1, $photos->count()); } + public function testBelongsToManyRelationshipModelsAreProperlyHydratedWithSoleQuery() + { + $user = EloquentTestUserWithCustomFriendPivot::create(['email' => 'taylorotwell@gmail.com']); + $user->friends()->create(['email' => 'abigailotwell@gmail.com']); + + $user->friends()->get()->each(function ($friend) { + $this->assertInstanceOf(EloquentTestFriendPivot::class, $friend->pivot); + }); + + $soleFriend = $user->friends()->where('email', 'abigailotwell@gmail.com')->sole(); + + $this->assertInstanceOf(EloquentTestFriendPivot::class, $soleFriend->pivot); + } + + public function testBelongsToManyRelationshipMissingModelExceptionWithSoleQueryWorks() + { + $this->expectException(ModelNotFoundException::class); + $user = EloquentTestUserWithCustomFriendPivot::create(['email' => 'taylorotwell@gmail.com']); + $user->friends()->where('email', 'abigailotwell@gmail.com')->sole(); + } + public function testBelongsToManyRelationshipModelsAreProperlyHydratedOverChunkedRequest() { $user = EloquentTestUser::create(['email' => 'taylorotwell@gmail.com']); @@ -1356,14 +1377,14 @@ public function testFreshMethodOnCollection() EloquentTestUser::find(2)->update(['email' => 'dev@mathieutu.ovh']); $this->assertCount(3, $users); - $this->assertNotEquals('Mathieu TUDISCO', $users[0]->name); - $this->assertNotEquals('dev@mathieutu.ovh', $users[1]->email); + $this->assertNotSame('Mathieu TUDISCO', $users[0]->name); + $this->assertNotSame('dev@mathieutu.ovh', $users[1]->email); $refreshedUsers = $users->fresh(); $this->assertCount(2, $refreshedUsers); - $this->assertEquals('Mathieu TUDISCO', $refreshedUsers[0]->name); - $this->assertEquals('dev@mathieutu.ovh', $refreshedUsers[1]->email); + $this->assertSame('Mathieu TUDISCO', $refreshedUsers[0]->name); + $this->assertSame('dev@mathieutu.ovh', $refreshedUsers[1]->email); } public function testTimestampsUsingDefaultDateFormat() @@ -1796,10 +1817,10 @@ public function testMorphPivotsCanBeRefreshed() ]); $this->assertInstanceOf(MorphPivot::class, $freshPivot = $pivot->fresh()); - $this->assertEquals('primary', $freshPivot->taxonomy); + $this->assertSame('primary', $freshPivot->taxonomy); $this->assertSame($pivot, $pivot->refresh()); - $this->assertEquals('primary', $pivot->taxonomy); + $this->assertSame('primary', $pivot->taxonomy); } /** diff --git a/tests/Database/DatabaseEloquentIrregularPluralTest.php b/tests/Database/DatabaseEloquentIrregularPluralTest.php index 8c73974d4565..4342ca5c541c 100644 --- a/tests/Database/DatabaseEloquentIrregularPluralTest.php +++ b/tests/Database/DatabaseEloquentIrregularPluralTest.php @@ -70,7 +70,7 @@ protected function schema() /** @test */ public function itPluralizesTheTableName() { - $model = new IrregularPluralHuman(); + $model = new IrregularPluralHuman; $this->assertSame('irregular_plural_humans', $model->getTable()); } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 42f3b6563927..4c2210be5106 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -72,7 +72,7 @@ public function testAttributeManipulation() public function testSetAttributeWithNumericKey() { - $model = new EloquentDateModelStub(); + $model = new EloquentDateModelStub; $model->setAttribute(0, 'value'); $this->assertEquals([0 => 'value'], $model->getAttributes()); @@ -95,7 +95,7 @@ public function testDirtyAttributes() public function testIntAndNullComparisonWhenDirty() { - $model = new EloquentModelCastingStub(); + $model = new EloquentModelCastingStub; $model->intAttribute = null; $model->syncOriginal(); $this->assertFalse($model->isDirty('intAttribute')); @@ -105,7 +105,7 @@ public function testIntAndNullComparisonWhenDirty() public function testFloatAndNullComparisonWhenDirty() { - $model = new EloquentModelCastingStub(); + $model = new EloquentModelCastingStub; $model->floatAttribute = null; $model->syncOriginal(); $this->assertFalse($model->isDirty('floatAttribute')); @@ -290,7 +290,16 @@ public function testDestroyMethodCallsQueryBuilderCorrectly() public function testDestroyMethodCallsQueryBuilderCorrectlyWithCollection() { - EloquentModelDestroyStub::destroy(new Collection([1, 2, 3])); + EloquentModelDestroyStub::destroy(new BaseCollection([1, 2, 3])); + } + + public function testDestroyMethodCallsQueryBuilderCorrectlyWithEloquentCollection() + { + EloquentModelDestroyStub::destroy(new Collection([ + new EloquentModelDestroyStub(['id' => 1]), + new EloquentModelDestroyStub(['id' => 2]), + new EloquentModelDestroyStub(['id' => 3]), + ])); } public function testDestroyMethodCallsQueryBuilderCorrectlyWithMultipleArgs() @@ -2101,7 +2110,7 @@ public function testNotTouchingModelWithoutTimestamps() public function testGetOriginalCastsAttributes() { - $model = new EloquentModelCastingStub(); + $model = new EloquentModelCastingStub; $model->intAttribute = '1'; $model->floatAttribute = '0.1234'; $model->stringAttribute = 432; @@ -2383,6 +2392,10 @@ public function newQuery() class EloquentModelDestroyStub extends Model { + protected $fillable = [ + 'id', + ]; + public function newQuery() { $mock = m::mock(Builder::class); diff --git a/tests/Database/DatabaseEloquentMorphToManyTest.php b/tests/Database/DatabaseEloquentMorphToManyTest.php index 92aae4c1f994..32ccbe4b6705 100644 --- a/tests/Database/DatabaseEloquentMorphToManyTest.php +++ b/tests/Database/DatabaseEloquentMorphToManyTest.php @@ -48,9 +48,9 @@ public function testDetachRemovesPivotTableRecord() $relation = $this->getMockBuilder(MorphToMany::class)->onlyMethods(['touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); $query = m::mock(stdClass::class); $query->shouldReceive('from')->once()->with('taggables')->andReturn($query); - $query->shouldReceive('where')->once()->with('taggable_id', 1)->andReturn($query); + $query->shouldReceive('where')->once()->with('taggables.taggable_id', 1)->andReturn($query); $query->shouldReceive('where')->once()->with('taggable_type', get_class($relation->getParent()))->andReturn($query); - $query->shouldReceive('whereIn')->once()->with('tag_id', [1, 2, 3]); + $query->shouldReceive('whereIn')->once()->with('taggables.tag_id', [1, 2, 3]); $query->shouldReceive('delete')->once()->andReturn(true); $relation->getQuery()->shouldReceive('getQuery')->andReturn($mockQueryBuilder = m::mock(stdClass::class)); $mockQueryBuilder->shouldReceive('newQuery')->once()->andReturn($query); @@ -64,7 +64,7 @@ public function testDetachMethodClearsAllPivotRecordsWhenNoIDsAreGiven() $relation = $this->getMockBuilder(MorphToMany::class)->onlyMethods(['touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); $query = m::mock(stdClass::class); $query->shouldReceive('from')->once()->with('taggables')->andReturn($query); - $query->shouldReceive('where')->once()->with('taggable_id', 1)->andReturn($query); + $query->shouldReceive('where')->once()->with('taggables.taggable_id', 1)->andReturn($query); $query->shouldReceive('where')->once()->with('taggable_type', get_class($relation->getParent()))->andReturn($query); $query->shouldReceive('whereIn')->never(); $query->shouldReceive('delete')->once()->andReturn(true); diff --git a/tests/Database/DatabaseEloquentMorphToTest.php b/tests/Database/DatabaseEloquentMorphToTest.php index 172a2aa84d1d..6895d3a656de 100644 --- a/tests/Database/DatabaseEloquentMorphToTest.php +++ b/tests/Database/DatabaseEloquentMorphToTest.php @@ -26,6 +26,12 @@ public function testLookupDictionaryIsProperlyConstructed() $one = (object) ['morph_type' => 'morph_type_1', 'foreign_key' => 'foreign_key_1'], $two = (object) ['morph_type' => 'morph_type_1', 'foreign_key' => 'foreign_key_1'], $three = (object) ['morph_type' => 'morph_type_2', 'foreign_key' => 'foreign_key_2'], + $four = (object) ['morph_type' => 'morph_type_2', 'foreign_key' => new class { + public function __toString() + { + return 'foreign_key_2'; + } + }], ]); $dictionary = $relation->getDictionary(); @@ -40,6 +46,7 @@ public function testLookupDictionaryIsProperlyConstructed() 'morph_type_2' => [ 'foreign_key_2' => [ $three, + $four, ], ], ], $dictionary); @@ -100,6 +107,16 @@ public function testMorphToWithZeroMorphType() $parent->relation(); } + public function testMorphToWithEmptyStringMorphType() + { + $parent = $this->getMockBuilder(EloquentMorphToModelStub::class)->onlyMethods(['getAttributeFromArray', 'morphEagerTo', 'morphInstanceTo'])->getMock(); + $parent->method('getAttributeFromArray')->with('relation_type')->willReturn(''); + $parent->expects($this->once())->method('morphEagerTo'); + $parent->expects($this->never())->method('morphInstanceTo'); + + $parent->relation(); + } + public function testMorphToWithSpecifiedClassDefault() { $parent = new EloquentMorphToModelStub; @@ -122,7 +139,7 @@ public function testAssociateMethodSetsForeignKeyAndTypeOnModel() $relation = $this->getRelationAssociate($parent); $associate = m::mock(Model::class); - $associate->shouldReceive('getKey')->once()->andReturn(1); + $associate->shouldReceive('getAttribute')->once()->andReturn(1); $associate->shouldReceive('getMorphClass')->once()->andReturn('Model'); $parent->shouldReceive('setAttribute')->once()->with('foreign_key', 1); diff --git a/tests/Database/DatabaseEloquentPivotTest.php b/tests/Database/DatabaseEloquentPivotTest.php index b2fbba69d99c..50beacb588da 100755 --- a/tests/Database/DatabaseEloquentPivotTest.php +++ b/tests/Database/DatabaseEloquentPivotTest.php @@ -156,7 +156,7 @@ public function testPivotModelWithoutParentReturnsModelTimestampColumns() public function testWithoutRelations() { - $original = new Pivot(); + $original = new Pivot; $original->pivotParent = 'foo'; $original->setRelation('bar', 'baz'); diff --git a/tests/Database/DatabaseMigrationRefreshCommandTest.php b/tests/Database/DatabaseMigrationRefreshCommandTest.php index dfb4180fe22c..8502fdfe8450 100755 --- a/tests/Database/DatabaseMigrationRefreshCommandTest.php +++ b/tests/Database/DatabaseMigrationRefreshCommandTest.php @@ -24,7 +24,7 @@ protected function tearDown(): void public function testRefreshCommandCallsCommandsWithProperArguments() { - $command = new RefreshCommand(); + $command = new RefreshCommand; $app = new ApplicationDatabaseRefreshStub(['path.database' => __DIR__]); $dispatcher = $app->instance(Dispatcher::class, $events = m::mock()); @@ -40,7 +40,7 @@ public function testRefreshCommandCallsCommandsWithProperArguments() $console->shouldReceive('find')->with('migrate')->andReturn($migrateCommand); $dispatcher->shouldReceive('dispatch')->once()->with(m::type(DatabaseRefreshed::class)); - $quote = DIRECTORY_SEPARATOR == '\\' ? '"' : "'"; + $quote = DIRECTORY_SEPARATOR === '\\' ? '"' : "'"; $resetCommand->shouldReceive('run')->with(new InputMatcher("--force=1 {$quote}migrate:reset{$quote}"), m::any()); $migrateCommand->shouldReceive('run')->with(new InputMatcher('--force=1 migrate'), m::any()); @@ -49,7 +49,7 @@ public function testRefreshCommandCallsCommandsWithProperArguments() public function testRefreshCommandCallsCommandsWithStep() { - $command = new RefreshCommand(); + $command = new RefreshCommand; $app = new ApplicationDatabaseRefreshStub(['path.database' => __DIR__]); $dispatcher = $app->instance(Dispatcher::class, $events = m::mock()); @@ -65,7 +65,7 @@ public function testRefreshCommandCallsCommandsWithStep() $console->shouldReceive('find')->with('migrate')->andReturn($migrateCommand); $dispatcher->shouldReceive('dispatch')->once()->with(m::type(DatabaseRefreshed::class)); - $quote = DIRECTORY_SEPARATOR == '\\' ? '"' : "'"; + $quote = DIRECTORY_SEPARATOR === '\\' ? '"' : "'"; $rollbackCommand->shouldReceive('run')->with(new InputMatcher("--step=2 --force=1 {$quote}migrate:rollback{$quote}"), m::any()); $migrateCommand->shouldReceive('run')->with(new InputMatcher('--force=1 migrate'), m::any()); diff --git a/tests/Database/DatabaseMySqlBuilderTest.php b/tests/Database/DatabaseMySqlBuilderTest.php new file mode 100644 index 000000000000..464ce75c741f --- /dev/null +++ b/tests/Database/DatabaseMySqlBuilderTest.php @@ -0,0 +1,48 @@ +shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8mb4'); + $connection->shouldReceive('getConfig')->once()->with('collation')->andReturn('utf8mb4_unicode_ci'); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'create database `my_temporary_database` default character set `utf8mb4` default collate `utf8mb4_unicode_ci`' + )->andReturn(true); + + $builder = new MySqlBuilder($connection); + $builder->createDatabase('my_temporary_database'); + } + + public function testDropDatabaseIfExists() + { + $grammar = new MySqlGrammar; + + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'drop database if exists `my_database_a`' + )->andReturn(true); + + $builder = new MySqlBuilder($connection); + + $builder->dropDatabaseIfExists('my_database_a'); + } +} diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index c89af64847ad..4c8813130677 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -504,6 +504,19 @@ public function testAddingColumnAfterAnotherColumn() $this->assertSame('alter table `users` add `name` varchar(255) not null after `foo`', $statements[0]); } + public function testAddingMultipleColumnsAfterAnotherColumn() + { + $blueprint = new Blueprint('users'); + $blueprint->after('foo', function ($blueprint) { + $blueprint->string('one'); + $blueprint->string('two'); + }); + $blueprint->string('three'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `one` varchar(255) not null after `foo`, add `two` varchar(255) not null after `one`, add `three` varchar(255) not null', $statements[0]); + } + public function testAddingGeneratedColumn() { $blueprint = new Blueprint('products'); @@ -1155,6 +1168,48 @@ public function testGrammarsAreMacroable() $this->assertTrue($c); } + public function testCreateDatabase() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8mb4_foo'); + $connection->shouldReceive('getConfig')->once()->once()->with('collation')->andReturn('utf8mb4_unicode_ci_foo'); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database `my_database_a` default character set `utf8mb4_foo` default collate `utf8mb4_unicode_ci_foo`', + $statement + ); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8mb4_bar'); + $connection->shouldReceive('getConfig')->once()->once()->with('collation')->andReturn('utf8mb4_unicode_ci_bar'); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database `my_database_b` default character set `utf8mb4_bar` default collate `utf8mb4_unicode_ci_bar`', + $statement + ); + } + + public function testDropDatabaseIfExists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists `my_database_a`', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists `my_database_b`', + $statement + ); + } + protected function getConnection() { return m::mock(Connection::class); diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php new file mode 100644 index 000000000000..6587d31ad2e6 --- /dev/null +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -0,0 +1,52 @@ +shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8'); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'create database "my_temporary_database" encoding "utf8"' + )->andReturn(true); + + $builder = $this->getBuilder($connection); + $builder->createDatabase('my_temporary_database'); + } + + public function testDropDatabaseIfExists() + { + $grammar = new PostgresGrammar; + + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'drop database if exists "my_database_a"' + )->andReturn(true); + + $builder = $this->getBuilder($connection); + + $builder->dropDatabaseIfExists('my_database_a'); + } + + protected function getBuilder($connection) + { + return new PostgresBuilder($connection); + } +} diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 052197fd08c4..3a7f80ae3865 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -977,6 +977,44 @@ public function testAddingMultiPolygon() $this->assertSame('alter table "geo" add column "coordinates" geography(multipolygon, 4326) not null', $statements[0]); } + public function testCreateDatabase() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8_foo'); + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database "my_database_a" encoding "utf8_foo"', + $statement + ); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8_bar'); + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database "my_database_b" encoding "utf8_bar"', + $statement + ); + } + + public function testDropDatabaseIfExists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists "my_database_b"', + $statement + ); + } + public function testDropAllTablesEscapesTableNames() { $statement = $this->getGrammar()->compileDropAllTables(['alpha', 'beta', 'gamma']); diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index ff013202564d..0fed6c3a1ba9 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -198,7 +198,7 @@ public function testWhenCallbackWithReturn() public function testWhenCallbackWithDefault() { $callback = function ($query, $condition) { - $this->assertEquals('truthy', $condition); + $this->assertSame('truthy', $condition); $query->where('id', '=', 1); }; @@ -263,7 +263,7 @@ public function testUnlessCallbackWithDefault() }; $default = function ($query, $condition) { - $this->assertEquals('truthy', $condition); + $this->assertSame('truthy', $condition); $query->where('id', '=', 2); }; @@ -301,24 +301,29 @@ public function testBasicWheres() public function testWheresWithArrayValue() { $builder = $this->getBuilder(); - $builder->select('*')->from('users')->where('id', [12, 30]); + $builder->select('*')->from('users')->where('id', [12]); $this->assertSame('select * from "users" where "id" = ?', $builder->toSql()); - $this->assertEquals([0 => 12, 1 => 30], $builder->getBindings()); + $this->assertEquals([0 => 12], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '=', [12, 30]); $this->assertSame('select * from "users" where "id" = ?', $builder->toSql()); - $this->assertEquals([0 => 12, 1 => 30], $builder->getBindings()); + $this->assertEquals([0 => 12], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '!=', [12, 30]); $this->assertSame('select * from "users" where "id" != ?', $builder->toSql()); - $this->assertEquals([0 => 12, 1 => 30], $builder->getBindings()); + $this->assertEquals([0 => 12], $builder->getBindings()); $builder = $this->getBuilder(); $builder->select('*')->from('users')->where('id', '<>', [12, 30]); $this->assertSame('select * from "users" where "id" <> ?', $builder->toSql()); - $this->assertEquals([0 => 12, 1 => 30], $builder->getBindings()); + $this->assertEquals([0 => 12], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('id', '=', [[12, 30]]); + $this->assertSame('select * from "users" where "id" = ?', $builder->toSql()); + $this->assertEquals([0 => 12], $builder->getBindings()); } public function testMySqlWrappingProtectsQuotationMarks() @@ -649,6 +654,16 @@ public function testWhereBetweens() $this->assertSame('select * from "users" where "id" between ? and ?', $builder->toSql()); $this->assertEquals([0 => 1, 1 => 2], $builder->getBindings()); + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->whereBetween('id', [[1, 2, 3]]); + $this->assertSame('select * from "users" where "id" between ? and ?', $builder->toSql()); + $this->assertEquals([0 => 1, 1 => 2], $builder->getBindings()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->whereBetween('id', [[1], [2, 3]]); + $this->assertSame('select * from "users" where "id" between ? and ?', $builder->toSql()); + $this->assertEquals([0 => 1, 1 => 2], $builder->getBindings()); + $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereNotBetween('id', [1, 2]); $this->assertSame('select * from "users" where "id" not between ? and ?', $builder->toSql()); @@ -1244,10 +1259,19 @@ public function testHavings() $builder = $this->getBuilder(); $builder->select(['category', new Raw('count(*) as "total"')])->from('item')->where('department', '=', 'popular')->groupBy('category')->having('total', '>', 3); $this->assertSame('select "category", count(*) as "total" from "item" where "department" = ? group by "category" having "total" > ?', $builder->toSql()); + } + + public function testHavingBetweens() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->havingBetween('id', [1, 2, 3]); + $this->assertSame('select * from "users" having "id" between ? and ?', $builder->toSql()); + $this->assertEquals([0 => 1, 1 => 2], $builder->getBindings()); $builder = $this->getBuilder(); - $builder->select('*')->from('users')->havingBetween('last_login_date', ['2018-11-16', '2018-12-16']); - $this->assertSame('select * from "users" having "last_login_date" between ? and ?', $builder->toSql()); + $builder->select('*')->from('users')->havingBetween('id', [[1, 2], [3, 4]]); + $this->assertSame('select * from "users" having "id" between ? and ?', $builder->toSql()); + $this->assertEquals([0 => 1, 1 => 2], $builder->getBindings()); } public function testHavingShortcut() @@ -1952,7 +1976,7 @@ public function testExistsOr() $builder = $this->getBuilder(); $builder->getConnection()->shouldReceive('select')->andReturn([['exists' => 0]]); $results = $builder->from('users')->doesntExistOr(function () { - throw new RuntimeException(); + throw new RuntimeException; }); $this->assertTrue($results); } @@ -1968,7 +1992,7 @@ public function testDoesntExistsOr() $builder = $this->getBuilder(); $builder->getConnection()->shouldReceive('select')->andReturn([['exists' => 1]]); $results = $builder->from('users')->existsOr(function () { - throw new RuntimeException(); + throw new RuntimeException; }); $this->assertTrue($results); } diff --git a/tests/Database/DatabaseSQLiteBuilderTest.php b/tests/Database/DatabaseSQLiteBuilderTest.php new file mode 100644 index 000000000000..4b4ab452dc35 --- /dev/null +++ b/tests/Database/DatabaseSQLiteBuilderTest.php @@ -0,0 +1,91 @@ +singleton('files', Filesystem::class); + + Facade::setFacadeApplication($app); + } + + protected function tearDown(): void + { + m::close(); + + Container::setInstance(null); + Facade::setFacadeApplication(null); + } + + public function testCreateDatabase() + { + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once(); + + $builder = new SQLiteBuilder($connection); + + File::shouldReceive('put') + ->once() + ->with('my_temporary_database_a', '') + ->andReturn(20); // bytes + + $this->assertTrue($builder->createDatabase('my_temporary_database_a')); + + File::shouldReceive('put') + ->once() + ->with('my_temporary_database_b', '') + ->andReturn(false); + + $this->assertFalse($builder->createDatabase('my_temporary_database_b')); + } + + public function testDropDatabaseIfExists() + { + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once(); + + $builder = new SQLiteBuilder($connection); + + File::shouldReceive('exists') + ->once() + ->andReturn(true); + + File::shouldReceive('delete') + ->once() + ->with('my_temporary_database_b') + ->andReturn(true); + + $this->assertTrue($builder->dropDatabaseIfExists('my_temporary_database_b')); + + File::shouldReceive('exists') + ->once() + ->andReturn(false); + + $this->assertTrue($builder->dropDatabaseIfExists('my_temporary_database_c')); + + File::shouldReceive('exists') + ->once() + ->andReturn(true); + + File::shouldReceive('delete') + ->once() + ->with('my_temporary_database_c') + ->andReturn(false); + + $this->assertFalse($builder->dropDatabaseIfExists('my_temporary_database_c')); + } +} diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 41e53cc725e7..226c58bf2b34 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -361,7 +361,7 @@ public function testAddingString() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table "users" add column "foo" varchar null default \'bar\'', $statements[0]); + $this->assertSame('alter table "users" add column "foo" varchar default \'bar\'', $statements[0]); } public function testAddingText() @@ -663,8 +663,8 @@ public function testAddingTimestamps() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(2, $statements); $this->assertEquals([ - 'alter table "users" add column "created_at" datetime null', - 'alter table "users" add column "updated_at" datetime null', + 'alter table "users" add column "created_at" datetime', + 'alter table "users" add column "updated_at" datetime', ], $statements); } @@ -675,8 +675,8 @@ public function testAddingTimestampsTz() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(2, $statements); $this->assertEquals([ - 'alter table "users" add column "created_at" datetime null', - 'alter table "users" add column "updated_at" datetime null', + 'alter table "users" add column "created_at" datetime', + 'alter table "users" add column "updated_at" datetime', ], $statements); } @@ -687,7 +687,7 @@ public function testAddingRememberToken() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table "users" add column "remember_token" varchar null', $statements[0]); + $this->assertSame('alter table "users" add column "remember_token" varchar', $statements[0]); } public function testAddingBinary() diff --git a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php b/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php index 794d6380a8ab..635921019d4c 100644 --- a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php @@ -169,7 +169,7 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintMySql->toSql($this->db->connection(), new MySqlGrammar()); + $queries = $blueprintMySql->toSql($this->db->connection(), new MySqlGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -186,7 +186,7 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintPostgres->toSql($this->db->connection(), new PostgresGrammar()); + $queries = $blueprintPostgres->toSql($this->db->connection(), new PostgresGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -203,7 +203,7 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintSQLite->toSql($this->db->connection(), new SQLiteGrammar()); + $queries = $blueprintSQLite->toSql($this->db->connection(), new SQLiteGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -220,7 +220,7 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintSqlServer->toSql($this->db->connection(), new SqlServerGrammar()); + $queries = $blueprintSqlServer->toSql($this->db->connection(), new SqlServerGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -244,7 +244,7 @@ public function testAddUniqueIndexWithNameWorks() $table->string('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintMySql->toSql($this->db->connection(), new MySqlGrammar()); + $queries = $blueprintMySql->toSql($this->db->connection(), new MySqlGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -261,7 +261,7 @@ public function testAddUniqueIndexWithNameWorks() $table->unsignedInteger('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintPostgres->toSql($this->db->connection(), new PostgresGrammar()); + $queries = $blueprintPostgres->toSql($this->db->connection(), new PostgresGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -278,7 +278,7 @@ public function testAddUniqueIndexWithNameWorks() $table->unsignedInteger('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintSQLite->toSql($this->db->connection(), new SQLiteGrammar()); + $queries = $blueprintSQLite->toSql($this->db->connection(), new SQLiteGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -295,7 +295,7 @@ public function testAddUniqueIndexWithNameWorks() $table->unsignedInteger('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintSqlServer->toSql($this->db->connection(), new SqlServerGrammar()); + $queries = $blueprintSqlServer->toSql($this->db->connection(), new SqlServerGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', diff --git a/tests/Database/DatabaseSchemaBuilderTest.php b/tests/Database/DatabaseSchemaBuilderTest.php index 53a13c79128c..b22bfd7dc70e 100755 --- a/tests/Database/DatabaseSchemaBuilderTest.php +++ b/tests/Database/DatabaseSchemaBuilderTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\Schema\Builder; +use LogicException; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -15,6 +16,32 @@ protected function tearDown(): void m::close(); } + public function testCreateDatabase() + { + $connection = m::mock(Connection::class); + $grammar = m::mock(stdClass::class); + $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); + $builder = new Builder($connection); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support creating databases.'); + + $builder->createDatabase('foo'); + } + + public function testDropDatabaseIfExists() + { + $connection = m::mock(Connection::class); + $grammar = m::mock(stdClass::class); + $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); + $builder = new Builder($connection); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support dropping databases.'); + + $builder->dropDatabaseIfExists('foo'); + } + public function testHasTableCorrectlyCallsGrammar() { $connection = m::mock(Connection::class); diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index 5ca3790522ab..72a072592b1a 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -894,6 +894,42 @@ public function testQuoteStringOnArray() $this->assertSame("N'中文', N'測試'", $this->getGrammar()->quoteString(['中文', '測試'])); } + public function testCreateDatabase() + { + $connection = $this->getConnection(); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database "my_database_b"', + $statement + ); + } + + public function testDropDatabaseIfExists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists "my_database_b"', + $statement + ); + } + protected function getConnection() { return m::mock(Connection::class); diff --git a/tests/Database/DatabaseTransactionsManagerTest.php b/tests/Database/DatabaseTransactionsManagerTest.php index 172a48e5a4a1..e8d82e048720 100755 --- a/tests/Database/DatabaseTransactionsManagerTest.php +++ b/tests/Database/DatabaseTransactionsManagerTest.php @@ -9,24 +9,24 @@ class DatabaseTransactionsManagerTest extends TestCase { public function testBeginningTransactions() { - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); $manager->begin('default', 2); $manager->begin('admin', 1); $this->assertCount(3, $manager->getTransactions()); - $this->assertEquals('default', $manager->getTransactions()[0]->connection); + $this->assertSame('default', $manager->getTransactions()[0]->connection); $this->assertEquals(1, $manager->getTransactions()[0]->level); - $this->assertEquals('default', $manager->getTransactions()[1]->connection); + $this->assertSame('default', $manager->getTransactions()[1]->connection); $this->assertEquals(2, $manager->getTransactions()[1]->level); - $this->assertEquals('admin', $manager->getTransactions()[2]->connection); + $this->assertSame('admin', $manager->getTransactions()[2]->connection); $this->assertEquals(1, $manager->getTransactions()[2]->level); } public function testRollingBackTransactions() { - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); $manager->begin('default', 2); @@ -36,16 +36,16 @@ public function testRollingBackTransactions() $this->assertCount(2, $manager->getTransactions()); - $this->assertEquals('default', $manager->getTransactions()[0]->connection); + $this->assertSame('default', $manager->getTransactions()[0]->connection); $this->assertEquals(1, $manager->getTransactions()[0]->level); - $this->assertEquals('admin', $manager->getTransactions()[1]->connection); + $this->assertSame('admin', $manager->getTransactions()[1]->connection); $this->assertEquals(1, $manager->getTransactions()[1]->level); } public function testRollingBackTransactionsAllTheWay() { - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); $manager->begin('default', 2); @@ -55,13 +55,13 @@ public function testRollingBackTransactionsAllTheWay() $this->assertCount(1, $manager->getTransactions()); - $this->assertEquals('admin', $manager->getTransactions()[0]->connection); + $this->assertSame('admin', $manager->getTransactions()[0]->connection); $this->assertEquals(1, $manager->getTransactions()[0]->level); } public function testCommittingTransactions() { - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); $manager->begin('default', 2); @@ -71,7 +71,7 @@ public function testCommittingTransactions() $this->assertCount(1, $manager->getTransactions()); - $this->assertEquals('admin', $manager->getTransactions()[0]->connection); + $this->assertSame('admin', $manager->getTransactions()[0]->connection); $this->assertEquals(1, $manager->getTransactions()[0]->level); } @@ -79,7 +79,7 @@ public function testCallbacksAreAddedToTheCurrentTransaction() { $callbacks = []; - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); @@ -102,7 +102,7 @@ public function testCommittingTransactionsExecutesCallbacks() { $callbacks = []; - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); @@ -129,7 +129,7 @@ public function testCommittingExecutesOnlyCallbacksOfTheConnection() { $callbacks = []; - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->begin('default', 1); @@ -154,7 +154,7 @@ public function testCallbackIsExecutedIfNoTransactions() { $callbacks = []; - $manager = (new DatabaseTransactionsManager()); + $manager = (new DatabaseTransactionsManager); $manager->addCallback(function () use (&$callbacks) { $callbacks[] = ['default', 1]; diff --git a/tests/Database/DatabaseTransactionsTest.php b/tests/Database/DatabaseTransactionsTest.php index 2385d6d26313..167dbaee6a48 100644 --- a/tests/Database/DatabaseTransactionsTest.php +++ b/tests/Database/DatabaseTransactionsTest.php @@ -2,10 +2,12 @@ namespace Illuminate\Tests\Database; +use Exception; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\DatabaseTransactionsManager; use Mockery as m; use PHPUnit\Framework\TestCase; +use Throwable; class DatabaseTransactionsTest extends TestCase { @@ -60,7 +62,7 @@ protected function tearDown(): void public function testTransactionIsRecordedAndCommitted() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('commit')->once()->with('default'); @@ -79,7 +81,7 @@ public function testTransactionIsRecordedAndCommitted() public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('commit')->once()->with('default'); @@ -98,7 +100,7 @@ public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods() public function testNestedTransactionIsRecordedAndCommitted() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('default', 2); $transactionManager->shouldReceive('commit')->once()->with('default'); @@ -124,7 +126,7 @@ public function testNestedTransactionIsRecordedAndCommitted() public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitted() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 1); $transactionManager->shouldReceive('begin')->once()->with('second_connection', 2); @@ -159,7 +161,7 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt public function testTransactionIsRolledBack() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('rollback')->once()->with('default', 0); $transactionManager->shouldNotReceive('commit'); @@ -176,15 +178,15 @@ public function testTransactionIsRolledBack() 'value' => 2, ]); - throw new \Exception; + throw new Exception; }); - } catch (\Throwable $e) { + } catch (Throwable $e) { } } public function testTransactionIsRolledBackUsingSeparateMethods() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('rollback')->once()->with('default', 0); $transactionManager->shouldNotReceive('commit'); @@ -206,7 +208,7 @@ public function testTransactionIsRolledBackUsingSeparateMethods() public function testNestedTransactionsAreRolledBack() { - $transactionManager = m::mock(new DatabaseTransactionsManager()); + $transactionManager = m::mock(new DatabaseTransactionsManager); $transactionManager->shouldReceive('begin')->once()->with('default', 1); $transactionManager->shouldReceive('begin')->once()->with('default', 2); $transactionManager->shouldReceive('rollback')->once()->with('default', 1); @@ -230,10 +232,10 @@ public function testNestedTransactionsAreRolledBack() 'value' => 2, ]); - throw new \Exception; + throw new Exception; }); }); - } catch (\Throwable $e) { + } catch (Throwable $e) { } } diff --git a/tests/Database/SqlServerBuilderTest.php b/tests/Database/SqlServerBuilderTest.php new file mode 100644 index 000000000000..039cadb8394a --- /dev/null +++ b/tests/Database/SqlServerBuilderTest.php @@ -0,0 +1,46 @@ +shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'create database "my_temporary_database_a"' + )->andReturn(true); + + $builder = new SqlServerBuilder($connection); + $builder->createDatabase('my_temporary_database_a'); + } + + public function testDropDatabaseIfExists() + { + $grammar = new SqlServerGrammar; + + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'drop database if exists "my_temporary_database_b"' + )->andReturn(true); + + $builder = new SqlServerBuilder($connection); + + $builder->dropDatabaseIfExists('my_temporary_database_b'); + } +} diff --git a/tests/Events/EventsDispatcherTest.php b/tests/Events/EventsDispatcherTest.php index f5ae19c95aa2..ea09fe576ca7 100755 --- a/tests/Events/EventsDispatcherTest.php +++ b/tests/Events/EventsDispatcherTest.php @@ -388,8 +388,7 @@ public function testBothClassesAndInterfacesWork() $this->assertSame('fooo', $_SERVER['__event.test1']); $this->assertSame('baar', $_SERVER['__event.test2']); - unset($_SERVER['__event.test1']); - unset($_SERVER['__event.test2']); + unset($_SERVER['__event.test1'], $_SERVER['__event.test2']); } public function testNestedEvent() diff --git a/tests/Events/EventsSubscriberTest.php b/tests/Events/EventsSubscriberTest.php index 2cc47f1aa114..2b69c47c45d1 100644 --- a/tests/Events/EventsSubscriberTest.php +++ b/tests/Events/EventsSubscriberTest.php @@ -27,7 +27,7 @@ public function testEventSubscribers() public function testEventSubscribeCanAcceptObject() { - $d = new Dispatcher(); + $d = new Dispatcher; $subs = m::mock(ExampleSubscriber::class); $subs->shouldReceive('subscribe')->once()->with($d); @@ -37,7 +37,7 @@ public function testEventSubscribeCanAcceptObject() public function testEventSubscribeCanReturnMappings() { - $d = new Dispatcher(); + $d = new Dispatcher; $d->subscribe(DeclarativeSubscriber::class); $d->dispatch('myEvent1'); diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index 148fcdb235bb..134432c291c0 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -39,7 +39,7 @@ public function testCustomizedQueuedEventHandlersAreQueued() { $d = new Dispatcher; - $fakeQueue = new QueueFake(new Container()); + $fakeQueue = new QueueFake(new Container); $d->setQueueResolver(function () use ($fakeQueue) { return $fakeQueue; @@ -55,7 +55,7 @@ public function testQueueIsSetByGetQueue() { $d = new Dispatcher; - $fakeQueue = new QueueFake(new Container()); + $fakeQueue = new QueueFake(new Container); $d->setQueueResolver(function () use ($fakeQueue) { return $fakeQueue; diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index d1ff89f680ce..4f0279718387 100755 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -137,7 +137,7 @@ public function testSetChmod() $files = new Filesystem; $files->chmod(self::$tempDir.'/file.txt', 0755); $filePermission = substr(sprintf('%o', fileperms(self::$tempDir.'/file.txt')), -4); - $expectedPermissions = DIRECTORY_SEPARATOR == '\\' ? '0666' : '0755'; + $expectedPermissions = DIRECTORY_SEPARATOR === '\\' ? '0666' : '0755'; $this->assertEquals($expectedPermissions, $filePermission); } @@ -148,7 +148,7 @@ public function testGetChmod() $files = new Filesystem; $filePermission = $files->chmod(self::$tempDir.'/file.txt'); - $expectedPermissions = DIRECTORY_SEPARATOR == '\\' ? '0666' : '0755'; + $expectedPermissions = DIRECTORY_SEPARATOR === '\\' ? '0666' : '0755'; $this->assertEquals($expectedPermissions, $filePermission); } @@ -488,7 +488,7 @@ public function testMakeDirectory() */ public function testSharedGet() { - if (PHP_OS == 'Darwin') { + if (PHP_OS === 'Darwin') { $this->markTestSkipped('The operating system is MacOS.'); } diff --git a/tests/Foundation/Bootstrap/LoadEnvironmentVariablesTest.php b/tests/Foundation/Bootstrap/LoadEnvironmentVariablesTest.php index 9720273fa8bb..e2e85801bc29 100644 --- a/tests/Foundation/Bootstrap/LoadEnvironmentVariablesTest.php +++ b/tests/Foundation/Bootstrap/LoadEnvironmentVariablesTest.php @@ -11,8 +11,7 @@ class LoadEnvironmentVariablesTest extends TestCase { protected function tearDown(): void { - unset($_ENV['FOO']); - unset($_SERVER['FOO']); + unset($_ENV['FOO'], $_SERVER['FOO']); putenv('FOO'); m::close(); } diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php index 0f0c4b636f21..51f1eac0f500 100755 --- a/tests/Foundation/FoundationApplicationTest.php +++ b/tests/Foundation/FoundationApplicationTest.php @@ -430,7 +430,7 @@ public function testEnvPathsAreUsedAndMadeAbsoluteForCachePathsWhenSpecifiedAsRe public function testEnvPathsAreUsedAndMadeAbsoluteForCachePathsWhenSpecifiedAsRelativeWithNullBasePath() { - $app = new Application(); + $app = new Application; $_SERVER['APP_SERVICES_CACHE'] = 'relative/path/services.php'; $_SERVER['APP_PACKAGES_CACHE'] = 'relative/path/packages.php'; $_SERVER['APP_CONFIG_CACHE'] = 'relative/path/config.php'; diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index 887de48f7ddd..90f701ece326 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract; use Illuminate\Contracts\Support\Responsable; use Illuminate\Contracts\View\Factory; +use Illuminate\Database\RecordsNotFoundException; use Illuminate\Foundation\Exceptions\Handler; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -75,6 +76,15 @@ public function testHandlerReportsExceptionAsContext() $this->handler->report(new RuntimeException('Exception message')); } + public function testHandlerCallsContextMethodIfPresent() + { + $logger = m::mock(LoggerInterface::class); + $this->container->instance(LoggerInterface::class, $logger); + $logger->shouldReceive('error')->withArgs(['Exception message', m::subset(['foo' => 'bar'])])->once(); + + $this->handler->report(new ContextProvidingException('Exception message')); + } + public function testHandlerReportsExceptionWhenUnReportable() { $logger = m::mock(LoggerInterface::class); @@ -97,6 +107,20 @@ public function testHandlerCallsReportMethodWithDependencies() $this->handler->report(new ReportableException('Exception message')); } + public function testHandlerReportsExceptionUsingCallableClass() + { + $reporter = m::mock(ReportingService::class); + $reporter->shouldReceive('send')->withArgs(['Exception message'])->once(); + + $logger = m::mock(LoggerInterface::class); + $this->container->instance(LoggerInterface::class, $logger); + $logger->shouldNotReceive('error'); + + $this->handler->reportable(new CustomReporter($reporter)); + + $this->handler->report(new CustomException('Exception message')); + } + public function testReturnsJsonWithStackTraceWhenAjaxRequestAndDebugTrue() { $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(true); @@ -124,6 +148,15 @@ public function testReturnsCustomResponseFromRenderableCallback() $this->assertSame('{"response":"My custom exception response"}', $response); } + public function testReturnsCustomResponseFromCallableClass() + { + $this->handler->renderable(new CustomRenderer); + + $response = $this->handler->render($this->request, new CustomException)->getContent(); + + $this->assertSame('{"response":"The CustomRenderer response"}', $response); + } + public function testReturnsCustomResponseWhenExceptionImplementsResponsable() { $response = $this->handler->render($this->request, new ResponsableException)->getContent(); @@ -235,6 +268,23 @@ public function testSuspiciousOperationReturns404WithoutReporting() $this->handler->report(new SuspiciousOperationException('Invalid method override "__CONSTRUCT"')); } + + public function testRecordsNotFoundReturns404WithoutReporting() + { + $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(true); + $this->request->shouldReceive('expectsJson')->once()->andReturn(true); + + $response = $this->handler->render($this->request, new RecordsNotFoundException); + + $this->assertEquals(404, $response->getStatusCode()); + $this->assertStringContainsString('"message": "Not found."', $response->getContent()); + + $logger = m::mock(LoggerInterface::class); + $this->container->instance(LoggerInterface::class, $logger); + $logger->shouldNotReceive('error'); + + $this->handler->report(new RecordsNotFoundException); + } } class CustomException extends Exception @@ -265,6 +315,41 @@ public function report() } } +class ContextProvidingException extends Exception +{ + public function context() + { + return [ + 'foo' => 'bar', + ]; + } +} + +class CustomReporter +{ + private $service; + + public function __construct(ReportingService $service) + { + $this->service = $service; + } + + public function __invoke(CustomException $e) + { + $this->service->send($e->getMessage()); + + return false; + } +} + +class CustomRenderer +{ + public function __invoke(CustomException $e, $request) + { + return response()->json(['response' => 'The CustomRenderer response']); + } +} + interface ReportingService { public function send($message); diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index fb535897f5ca..d394566ce6cb 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -119,15 +119,15 @@ public function test_after_validation_runs_after_validation() * Catch the given exception thrown from the executor, and return it. * * @param string $class - * @param \Closure $excecutor + * @param \Closure $executor * @return \Exception * * @throws \Exception */ - protected function catchException($class, $excecutor) + protected function catchException($class, $executor) { try { - $excecutor(); + $executor(); } catch (Exception $e) { if (is_a($e, $class)) { return $e; diff --git a/tests/Foundation/Http/Middleware/ConvertEmptyStringsToNullTest.php b/tests/Foundation/Http/Middleware/ConvertEmptyStringsToNullTest.php index 6e3d767b125d..02913813e7bc 100644 --- a/tests/Foundation/Http/Middleware/ConvertEmptyStringsToNullTest.php +++ b/tests/Foundation/Http/Middleware/ConvertEmptyStringsToNullTest.php @@ -11,7 +11,7 @@ class ConvertEmptyStringsToNullTest extends TestCase { public function testConvertsEmptyStringsToNull() { - $middleware = new ConvertEmptyStringsToNull(); + $middleware = new ConvertEmptyStringsToNull; $symfonyRequest = new SymfonyRequest([ 'foo' => 'bar', 'baz' => '', diff --git a/tests/Foundation/Http/Middleware/TrimStringsTest.php b/tests/Foundation/Http/Middleware/TrimStringsTest.php index 1561eab6b03c..33262af684f7 100644 --- a/tests/Foundation/Http/Middleware/TrimStringsTest.php +++ b/tests/Foundation/Http/Middleware/TrimStringsTest.php @@ -11,7 +11,7 @@ class TrimStringsTest extends TestCase { public function testTrimStringsIgnoringExceptAttribute() { - $middleware = new TrimStringsWithExceptAttribute(); + $middleware = new TrimStringsWithExceptAttribute; $symfonyRequest = new SymfonyRequest([ 'abc' => ' 123 ', 'xyz' => ' 456 ', diff --git a/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php b/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php index cfe4460ad07d..f4ae9c17818b 100644 --- a/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php +++ b/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Mix; use Orchestra\Testbench\TestCase; +use stdClass; class InteractsWithContainerTest extends TestCase { @@ -17,7 +18,7 @@ public function testWithoutMixBindsEmptyHandlerAndReturnsInstance() public function testWithMixRestoresOriginalHandlerAndReturnsInstance() { - $handler = new \stdClass(); + $handler = new stdClass; $this->app->instance(Mix::class, $handler); $this->withoutMix(); diff --git a/tests/Foundation/Testing/WormholeTest.php b/tests/Foundation/Testing/WormholeTest.php index 13174e8f189f..e6fbfa13edcf 100644 --- a/tests/Foundation/Testing/WormholeTest.php +++ b/tests/Foundation/Testing/WormholeTest.php @@ -11,17 +11,17 @@ class WormholeTest extends TestCase { public function testCanTravelBackToPresent() { - // Preserve the timelines we want to compare the reality with.. + // Preserve the timelines we want to compare the reality with... $present = now(); $future = now()->addDays(10); - // Travel in time.. + // Travel in time... (new Wormhole(10))->days(); - // Assert we are now in the future.. + // Assert we are now in the future... $this->assertEquals($future->format('Y-m-d'), now()->format('Y-m-d')); - // Assert we can go back to the present.. + // Assert we can go back to the present... $this->assertEquals($present->format('Y-m-d'), Wormhole::back()->format('Y-m-d')); } diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 12722b52b020..63e07bfbf7e1 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -7,9 +7,13 @@ use Illuminate\Http\Client\Request; use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\Response; +use Illuminate\Http\Client\ResponseSequence; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use OutOfBoundsException; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\VarDumper; class HttpClientTest extends TestCase { @@ -54,6 +58,21 @@ public function testResponseBodyCasting() $this->assertSame('bar', $response->object()->result->foo); } + public function testResponseCanBeReturnedAsCollection() + { + $this->factory->fake([ + '*' => ['result' => ['foo' => 'bar']], + ]); + + $response = $this->factory->get('http://foo.com/api'); + + $this->assertInstanceOf(Collection::class, $response->collect()); + $this->assertEquals(collect(['result' => ['foo' => 'bar']]), $response->collect()); + $this->assertEquals(collect(['foo' => 'bar']), $response->collect('result')); + $this->assertEquals(collect(['bar']), $response->collect('result.foo')); + $this->assertEquals(collect(), $response->collect('missing_key')); + } + public function testUrlsCanBeStubbedByPath() { $this->factory->fake([ @@ -642,7 +661,7 @@ public function testAssertionsSentOutOfOrderThrowAssertionFailed() $this->factory->get($exampleUrls[2]); $this->factory->get($exampleUrls[1]); - $this->expectException(\PHPUnit\Framework\AssertionFailedError::class); + $this->expectException(AssertionFailedError::class); $this->factory->assertSentInOrder($exampleUrls); } @@ -660,7 +679,7 @@ public function testWrongNumberOfRequestsThrowAssertionFailed() $this->factory->get($exampleUrls[0]); $this->factory->get($exampleUrls[1]); - $this->expectException(\PHPUnit\Framework\AssertionFailedError::class); + $this->expectException(AssertionFailedError::class); $this->factory->assertSentInOrder($exampleUrls); } @@ -671,13 +690,13 @@ public function testCanAssertAgainstOrderOfHttpRequestsWithCallables() $exampleUrls = [ function ($request) { - return $request->url() == 'http://example.com/1'; + return $request->url() === 'http://example.com/1'; }, function ($request) { - return $request->url() == 'http://example.com/2'; + return $request->url() === 'http://example.com/2'; }, function ($request) { - return $request->url() == 'http://example.com/3'; + return $request->url() === 'http://example.com/3'; }, ]; @@ -761,8 +780,36 @@ function (Request $request) { 'name' => 'Taylor', ]); - $this->expectException(\PHPUnit\Framework\AssertionFailedError::class); + $this->expectException(AssertionFailedError::class); $this->factory->assertSentInOrder($executionOrder); } + + public function testCanDump() + { + $dumped = []; + + VarDumper::setHandler(function ($value) use (&$dumped) { + $dumped[] = $value; + }); + + $this->factory->fake()->dump(1, 2, 3)->withOptions(['delay' => 1000])->get('http://foo.com'); + + $this->assertSame(1, $dumped[0]); + $this->assertSame(2, $dumped[1]); + $this->assertSame(3, $dumped[2]); + $this->assertInstanceOf(Request::class, $dumped[3]); + $this->assertSame(1000, $dumped[4]['delay']); + + VarDumper::setHandler(null); + } + + public function testResponseSequenceIsMacroable() + { + ResponseSequence::macro('customMethod', function () { + return 'yes!'; + }); + + $this->assertSame('yes!', $this->factory->fakeSequence()->customMethod()); + } } diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index a381c4871192..c019499419f6 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -501,8 +501,8 @@ public function testArrayAccess() return $route; }); - $this->assertFalse(isset($request['non-existant'])); - $this->assertNull($request['non-existant']); + $this->assertFalse(isset($request['non-existent'])); + $this->assertNull($request['non-existent']); $this->assertTrue(isset($request['name'])); $this->assertNull($request['name']); @@ -1024,7 +1024,7 @@ public function testMagicMethods() // Parameter 'foo' is 'bar', then it ISSET and is NOT EMPTY. $this->assertSame('bar', $request->foo); $this->assertTrue(isset($request->foo)); - $this->assertFalse(empty($request->foo)); + $this->assertNotEmpty($request->foo); // Parameter 'empty' is '', then it ISSET and is EMPTY. $this->assertSame('', $request->empty); @@ -1034,7 +1034,7 @@ public function testMagicMethods() // Parameter 'undefined' is undefined/null, then it NOT ISSET and is EMPTY. $this->assertNull($request->undefined); $this->assertFalse(isset($request->undefined)); - $this->assertTrue(empty($request->undefined)); + $this->assertEmpty($request->undefined); // Simulates Route parameters. $request = Request::create('/example/bar', 'GET', ['xyz' => 'overwritten']); @@ -1049,18 +1049,18 @@ public function testMagicMethods() $this->assertSame('bar', $request->foo); $this->assertSame('bar', $request['foo']); $this->assertTrue(isset($request->foo)); - $this->assertFalse(empty($request->foo)); + $this->assertNotEmpty($request->foo); // Router parameter 'undefined' is undefined/null, then it NOT ISSET and is EMPTY. $this->assertNull($request->undefined); $this->assertFalse(isset($request->undefined)); - $this->assertTrue(empty($request->undefined)); + $this->assertEmpty($request->undefined); // Special case: router parameter 'xyz' is 'overwritten' by QueryString, then it ISSET and is NOT EMPTY. // Basically, QueryStrings have priority over router parameters. $this->assertSame('overwritten', $request->xyz); $this->assertTrue(isset($request->foo)); - $this->assertFalse(empty($request->foo)); + $this->assertNotEmpty($request->foo); // Simulates empty QueryString and Routes. $request = Request::create('/', 'GET'); @@ -1074,7 +1074,7 @@ public function testMagicMethods() // Parameter 'undefined' is undefined/null, then it NOT ISSET and is EMPTY. $this->assertNull($request->undefined); $this->assertFalse(isset($request->undefined)); - $this->assertTrue(empty($request->undefined)); + $this->assertEmpty($request->undefined); // Special case: simulates empty QueryString and Routes, without the Route Resolver. // It'll happen when you try to get a parameter outside a route. @@ -1083,7 +1083,7 @@ public function testMagicMethods() // Parameter 'undefined' is undefined/null, then it NOT ISSET and is EMPTY. $this->assertNull($request->undefined); $this->assertFalse(isset($request->undefined)); - $this->assertTrue(empty($request->undefined)); + $this->assertEmpty($request->undefined); } public function testHttpRequestFlashCallsSessionFlashInputWithInputData() diff --git a/tests/Integration/Auth/AuthenticationTest.php b/tests/Integration/Auth/AuthenticationTest.php index ec23afba3d18..6f3dbce1a343 100644 --- a/tests/Integration/Auth/AuthenticationTest.php +++ b/tests/Integration/Auth/AuthenticationTest.php @@ -19,6 +19,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Testing\Fakes\EventFake; use Illuminate\Tests\Integration\Auth\Fixtures\AuthenticationTestUser; +use InvalidArgumentException; use Orchestra\Testbench\TestCase; /** @@ -211,7 +212,7 @@ public function testLoggingOutOtherDevices() $this->assertEquals(1, $user->id); - $this->app['auth']->logoutOtherDevices('adifferentpassword'); + $this->app['auth']->logoutOtherDevices('password'); $this->assertEquals(1, $user->id); Event::assertDispatched(OtherDeviceLogout::class, function ($event) { @@ -222,6 +223,20 @@ public function testLoggingOutOtherDevices() }); } + public function testPasswordMustBeValidToLogOutOtherDevices() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('current password'); + + $this->app['auth']->loginUsingId(1); + + $user = $this->app['auth']->user(); + + $this->assertEquals(1, $user->id); + + $this->app['auth']->logoutOtherDevices('adifferentpassword'); + } + public function testLoggingInOutViaAttemptRemembering() { $this->assertTrue( @@ -298,7 +313,7 @@ public function testDispatcherChangesIfThereIsOneOnTheCustomAuthGuard() ]; Auth::extend('myCustomDriver', function () { - return new MyCustomGuardStub(); + return new MyCustomGuardStub; }); $this->assertInstanceOf(MyCustomGuardStub::class, $this->app['auth']->guard('myGuard')); @@ -318,7 +333,7 @@ public function testHasNoProblemIfThereIsNoDispatchingTheAuthCustomGuard() ]; Auth::extend('myCustomDriver', function () { - return new MyDispatcherLessCustomGuardStub(); + return new MyDispatcherLessCustomGuardStub; }); $this->assertInstanceOf(MyDispatcherLessCustomGuardStub::class, $this->app['auth']->guard('myGuard')); @@ -335,7 +350,7 @@ class MyCustomGuardStub public function __construct() { - $this->setDispatcher(new Dispatcher()); + $this->setDispatcher(new Dispatcher); } public function setDispatcher(Dispatcher $events) diff --git a/tests/Integration/Auth/GatePolicyResolutionTest.php b/tests/Integration/Auth/GatePolicyResolutionTest.php index 9781d59c8a7c..349bd9f89022 100644 --- a/tests/Integration/Auth/GatePolicyResolutionTest.php +++ b/tests/Integration/Auth/GatePolicyResolutionTest.php @@ -2,6 +2,8 @@ namespace Illuminate\Tests\Integration\Auth; +use Illuminate\Auth\Access\Events\GateEvaluated; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Tests\Integration\Auth\Fixtures\AuthenticationTestUser; use Illuminate\Tests\Integration\Auth\Fixtures\Policies\AuthenticationTestUserPolicy; @@ -12,6 +14,15 @@ */ class GatePolicyResolutionTest extends TestCase { + public function testGateEvaluationEventIsFired() + { + Event::fake(); + + Gate::check('foo'); + + Event::assertDispatched(GateEvaluated::class); + } + public function testPolicyCanBeGuessedUsingClassConventions() { $this->assertInstanceOf( diff --git a/tests/Integration/Cache/DynamoDbStoreTest.php b/tests/Integration/Cache/DynamoDbStoreTest.php index 74897fbde8cb..f7aeae6a3deb 100644 --- a/tests/Integration/Cache/DynamoDbStoreTest.php +++ b/tests/Integration/Cache/DynamoDbStoreTest.php @@ -2,6 +2,8 @@ namespace Illuminate\Tests\Integration\Cache; +use Aws\DynamoDb\DynamoDbClient; +use Aws\Exception\AwsException; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; @@ -13,11 +15,11 @@ class DynamoDbStoreTest extends TestCase { protected function setUp(): void { - parent::setUp(); - if (! env('DYNAMODB_CACHE_TABLE')) { $this->markTestSkipped('DynamoDB not configured.'); } + + parent::setUp(); } public function testItemsCanBeStoredAndRetrieved() @@ -74,15 +76,63 @@ public function testLocksCanBeAcquired() */ protected function getEnvironmentSetUp($app) { + if (! env('DYNAMODB_CACHE_TABLE')) { + $this->markTestSkipped('DynamoDB not configured.'); + } + $app['config']->set('cache.default', 'dynamodb'); - $app['config']->set('cache.stores.dynamodb', [ - 'driver' => 'dynamodb', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => 'us-east-1', - 'table' => env('DYNAMODB_CACHE_TABLE', 'laravel_test'), - 'endpoint' => env('DYNAMODB_ENDPOINT'), + $config = $app['config']->get('cache.stores.dynamodb'); + + /** @var \Aws\DynamoDb\DynamoDbClient $client */ + $client = $app['cache.dynamodb.client']; + + if ($this->dynamoTableExists($client, $config['table'])) { + return; + } + + $client->createTable([ + 'TableName' => $config['table'], + 'KeySchema' => [ + [ + 'AttributeName' => $config['attributes']['key'] ?? 'key', + 'KeyType' => 'HASH', + ], + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $config['attributes']['key'] ?? 'key', + 'AttributeType' => 'S', + ], + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 1, + 'WriteCapacityUnits' => 1, + ], ]); } + + /** + * Determine if the given DynamoDB table exists. + * + * @param \Aws\DynamoDb\DynamoDbClient $client + * @param string $table + * @return bool + */ + public function dynamoTableExists(DynamoDbClient $client, $table) + { + try { + $client->describeTable([ + 'TableName' => $table, + ]); + + return true; + } catch (AwsException $e) { + if (Str::contains($e->getAwsErrorMessage(), ['resource not found', 'Cannot do operations on a non-existent table'])) { + return false; + } + + throw $e; + } + } } diff --git a/tests/Integration/Cache/PhpRedisCacheLockTest.php b/tests/Integration/Cache/PhpRedisCacheLockTest.php new file mode 100644 index 000000000000..de1048eb30c4 --- /dev/null +++ b/tests/Integration/Cache/PhpRedisCacheLockTest.php @@ -0,0 +1,306 @@ +setUpRedis(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->tearDownRedis(); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithoutSerializationAndCompression() + { + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithPhpSerialization() + { + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithJsonSerialization() + { + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithIgbinarySerialization() + { + if (! defined('Redis::SERIALIZER_IGBINARY')) { + $this->markTestSkipped('Redis extension is not configured to support the igbinary serializer.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithMsgpackSerialization() + { + if (! defined('Redis::SERIALIZER_MSGPACK')) { + $this->markTestSkipped('Redis extension is not configured to support the msgpack serializer.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_MSGPACK); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithLzfCompression() + { + if (! defined('Redis::COMPRESSION_LZF')) { + $this->markTestSkipped('Redis extension is not configured to support the lzf compression.'); + } + + if (! extension_loaded('lzf')) { + $this->markTestSkipped('Lzf extension is not installed.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZF); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithZstdCompression() + { + if (! defined('Redis::COMPRESSION_ZSTD')) { + $this->markTestSkipped('Redis extension is not configured to support the zstd compression.'); + } + + if (! extension_loaded('zstd')) { + $this->markTestSkipped('Zstd extension is not installed.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_ZSTD); + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, Redis::COMPRESSION_ZSTD_DEFAULT); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, Redis::COMPRESSION_ZSTD_MIN); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, Redis::COMPRESSION_ZSTD_MAX); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithLz4Compression() + { + if (! defined('Redis::COMPRESSION_LZ4')) { + $this->markTestSkipped('Redis extension is not configured to support the lz4 compression.'); + } + + if (! extension_loaded('lz4')) { + $this->markTestSkipped('Lz4 extension is not installed.'); + } + + $this->markTestIncomplete( + 'phpredis extension does not compress consistently with the php '. + 'extension lz4. See: https://github.com/phpredis/phpredis/issues/1939' + ); + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZ4); + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, 1); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, 3); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, 12); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithSerializationAndCompression() + { + if (! defined('Redis::COMPRESSION_LZF')) { + $this->markTestSkipped('Redis extension is not configured to support the lzf compression.'); + } + + if (! extension_loaded('lzf')) { + $this->markTestSkipped('Lzf extension is not installed.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZF); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } +} diff --git a/tests/Integration/Database/DatabaseArrayObjectAndCollectionCustomCastTest.php b/tests/Integration/Database/DatabaseArrayObjectAndCollectionCustomCastTest.php new file mode 100644 index 000000000000..7f5b8e30f220 --- /dev/null +++ b/tests/Integration/Database/DatabaseArrayObjectAndCollectionCustomCastTest.php @@ -0,0 +1,75 @@ +increments('id'); + $table->text('array_object'); + $table->text('collection'); + $table->timestamps(); + }); + } + + public function test_array_object_and_collection_casting() + { + $model = new TestEloquentModelWithCustomArrayObjectCast; + + $model->array_object = ['name' => 'Taylor']; + $model->collection = collect(['name' => 'Taylor']); + + $model->save(); + + $model = $model->fresh(); + + $this->assertEquals(['name' => 'Taylor'], $model->array_object->toArray()); + $this->assertEquals(['name' => 'Taylor'], $model->collection->toArray()); + + $model->array_object['age'] = 34; + $model->array_object['meta']['title'] = 'Developer'; + + $model->save(); + + $model = $model->fresh(); + + $this->assertEquals([ + 'name' => 'Taylor', + 'age' => 34, + 'meta' => ['title' => 'Developer'], + ], $model->array_object->toArray()); + } +} + +class TestEloquentModelWithCustomArrayObjectCast extends Model +{ + /** + * The attributes that aren't mass assignable. + * + * @var string[] + */ + protected $guarded = []; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'array_object' => AsArrayObject::class, + 'collection' => AsCollection::class, + ]; +} diff --git a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php index bbbb1bc3e50d..c2966b21dc28 100644 --- a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php @@ -106,7 +106,7 @@ public function testBasicCustomCasting() $model = new TestEloquentModelWithCustomCast; $model->birthday_at = now(); - $this->assertTrue(is_string($model->toArray()['birthday_at'])); + $this->assertIsString($model->toArray()['birthday_at']); } public function testGetOriginalWithCastValueObjects() @@ -447,7 +447,7 @@ class ValueObjectWithCasterInstance extends ValueObject { public static function castUsing(array $arguments) { - return new ValueObjectCaster(); + return new ValueObjectCaster; } } diff --git a/tests/Integration/Database/DatabaseSchemaBuilderAlterTableWithEnumTest.php b/tests/Integration/Database/DatabaseSchemaBuilderAlterTableWithEnumTest.php index 92c0f30bb44e..f2a8cb201323 100644 --- a/tests/Integration/Database/DatabaseSchemaBuilderAlterTableWithEnumTest.php +++ b/tests/Integration/Database/DatabaseSchemaBuilderAlterTableWithEnumTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use stdClass; class DatabaseSchemaBuilderAlterTableWithEnumTest extends DatabaseMySqlTestCase { @@ -30,7 +31,7 @@ public function testGetAllTablesAndColumnListing() $tables = Schema::getAllTables(); $this->assertCount(1, $tables); - $this->assertEquals('stdClass', get_class($tables[0])); + $this->assertInstanceOf(stdClass::class, $tables[0]); $tableProperties = array_values((array) $tables[0]); $this->assertEquals(['users', 'BASE TABLE'], $tableProperties); diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index b66cd3d3f34c..6ab1dd6f55fd 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -349,7 +349,7 @@ public function testFindMethod() $this->assertEquals($tag2->name, $post->tags()->find($tag2->id)->name); $this->assertCount(0, $post->tags()->findMany([])); $this->assertCount(2, $post->tags()->findMany([$tag->id, $tag2->id])); - $this->assertCount(0, $post->tags()->findMany(new Collection())); + $this->assertCount(0, $post->tags()->findMany(new Collection)); $this->assertCount(2, $post->tags()->findMany(new Collection([$tag->id, $tag2->id]))); } diff --git a/tests/Integration/Database/EloquentBelongsToTest.php b/tests/Integration/Database/EloquentBelongsToTest.php index e04e39deba50..eb7a59e27f97 100644 --- a/tests/Integration/Database/EloquentBelongsToTest.php +++ b/tests/Integration/Database/EloquentBelongsToTest.php @@ -96,7 +96,7 @@ public function testParentIsModel() public function testParentIsNotAnotherModel() { $child = User::has('parent')->first(); - $parent = new User(); + $parent = new User; $parent->id = 3; $this->assertFalse($child->parent()->is($parent)); diff --git a/tests/Integration/Database/EloquentHasOneIsTest.php b/tests/Integration/Database/EloquentHasOneIsTest.php index 74f76e56ffe6..85c7db998aa0 100644 --- a/tests/Integration/Database/EloquentHasOneIsTest.php +++ b/tests/Integration/Database/EloquentHasOneIsTest.php @@ -51,7 +51,7 @@ public function testChildIsModel() public function testChildIsNotAnotherModel() { $parent = Post::first(); - $child = new Attachment(); + $child = new Attachment; $child->id = 2; $this->assertFalse($parent->attachment()->is($child)); diff --git a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php index 6eca8078028c..abe44ad7a2b5 100644 --- a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php +++ b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Schema; +use stdClass; /** * @group integration @@ -98,9 +99,39 @@ public function testJsonIsCastable() ]); } + public function testJsonAttributeIsCastable() + { + $this->encrypter->expects('encrypt') + ->with('{"key1":"value1"}', false) + ->andReturn('encrypted-secret-json-string'); + $this->encrypter->expects('decrypt') + ->with('encrypted-secret-json-string', false) + ->andReturn('{"key1":"value1"}'); + $this->encrypter->expects('encrypt') + ->with('{"key1":"value1","key2":"value2"}', false) + ->andReturn('encrypted-secret-json-string2'); + $this->encrypter->expects('decrypt') + ->with('encrypted-secret-json-string2', false) + ->andReturn('{"key1":"value1","key2":"value2"}'); + + $subject = new EncryptedCast([ + 'secret_json' => ['key1' => 'value1'], + ]); + $subject->fill([ + 'secret_json->key2' => 'value2', + ]); + $subject->save(); + + $this->assertSame(['key1' => 'value1', 'key2' => 'value2'], $subject->secret_json); + $this->assertDatabaseHas('encrypted_casts', [ + 'id' => $subject->id, + 'secret_json' => 'encrypted-secret-json-string2', + ]); + } + public function testObjectIsCastable() { - $object = new \stdClass(); + $object = new stdClass; $object->key1 = 'value1'; $this->encrypter->expects('encrypt') @@ -116,7 +147,7 @@ public function testObjectIsCastable() 'secret_object' => $object, ]); - $this->assertInstanceOf(\stdClass::class, $object->secret_object); + $this->assertInstanceOf(stdClass::class, $object->secret_object); $this->assertSame('value1', $object->secret_object->key1); $this->assertDatabaseHas('encrypted_casts', [ 'id' => $object->id, diff --git a/tests/Integration/Database/EloquentModelJsonCastingTest.php b/tests/Integration/Database/EloquentModelJsonCastingTest.php index a7487333916e..48707a89c7c8 100644 --- a/tests/Integration/Database/EloquentModelJsonCastingTest.php +++ b/tests/Integration/Database/EloquentModelJsonCastingTest.php @@ -52,7 +52,7 @@ public function testArraysAreCastable() public function testObjectsAreCastable() { - $object = new stdClass(); + $object = new stdClass; $object->key1 = 'value1'; /** @var \Illuminate\Tests\Integration\Database\EloquentModelJsonCastingTest\JsonCast $user */ diff --git a/tests/Integration/Database/EloquentModelLoadCountTest.php b/tests/Integration/Database/EloquentModelLoadCountTest.php index ac3e926f7c5d..92c46d540a8b 100644 --- a/tests/Integration/Database/EloquentModelLoadCountTest.php +++ b/tests/Integration/Database/EloquentModelLoadCountTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Database\EloquentModelLoadCountTest; +use DB; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Schema\Blueprint; @@ -49,11 +50,11 @@ public function testLoadCountSingleRelation() { $model = BaseModel::first(); - \DB::enableQueryLog(); + DB::enableQueryLog(); $model->loadCount('related1'); - $this->assertCount(1, \DB::getQueryLog()); + $this->assertCount(1, DB::getQueryLog()); $this->assertEquals(2, $model->related1_count); } @@ -61,11 +62,11 @@ public function testLoadCountMultipleRelations() { $model = BaseModel::first(); - \DB::enableQueryLog(); + DB::enableQueryLog(); $model->loadCount(['related1', 'related2']); - $this->assertCount(1, \DB::getQueryLog()); + $this->assertCount(1, DB::getQueryLog()); $this->assertEquals(2, $model->related1_count); $this->assertEquals(1, $model->related2_count); } diff --git a/tests/Integration/Database/EloquentModelRefreshTest.php b/tests/Integration/Database/EloquentModelRefreshTest.php index 18b648d7f0d4..626bacab5cbb 100644 --- a/tests/Integration/Database/EloquentModelRefreshTest.php +++ b/tests/Integration/Database/EloquentModelRefreshTest.php @@ -100,7 +100,7 @@ class AsPivotPost extends Post public function children() { return $this - ->belongsToMany(static::class, (new AsPivotPostPivot())->getTable(), 'foreign_id', 'related_id') + ->belongsToMany(static::class, (new AsPivotPostPivot)->getTable(), 'foreign_id', 'related_id') ->using(AsPivotPostPivot::class); } } diff --git a/tests/Integration/Database/EloquentMorphOneIsTest.php b/tests/Integration/Database/EloquentMorphOneIsTest.php index 7d670264aac2..96d40842e436 100644 --- a/tests/Integration/Database/EloquentMorphOneIsTest.php +++ b/tests/Integration/Database/EloquentMorphOneIsTest.php @@ -52,7 +52,7 @@ public function testChildIsModel() public function testChildIsNotAnotherModel() { $parent = Post::first(); - $child = new Attachment(); + $child = new Attachment; $child->id = 2; $this->assertFalse($parent->attachment()->is($child)); diff --git a/tests/Integration/Database/EloquentMorphToIsTest.php b/tests/Integration/Database/EloquentMorphToIsTest.php index dbc33ed48c73..fa2daaf1a7f0 100644 --- a/tests/Integration/Database/EloquentMorphToIsTest.php +++ b/tests/Integration/Database/EloquentMorphToIsTest.php @@ -52,7 +52,7 @@ public function testParentIsModel() public function testParentIsNotAnotherModel() { $child = Comment::first(); - $parent = new Post(); + $parent = new Post; $parent->id = 2; $this->assertFalse($child->commentable()->is($parent)); diff --git a/tests/Integration/Database/EloquentRelationshipsTest.php b/tests/Integration/Database/EloquentRelationshipsTest.php index 650d7e41c59d..155ed617e5ad 100644 --- a/tests/Integration/Database/EloquentRelationshipsTest.php +++ b/tests/Integration/Database/EloquentRelationshipsTest.php @@ -56,11 +56,11 @@ public function testOverriddenRelationships() public function testAlwaysUnsetBelongsToRelationWhenReceivedModelId() { // create users - $user1 = (new FakeRelationship())->forceFill(['id' => 1]); - $user2 = (new FakeRelationship())->forceFill(['id' => 2]); + $user1 = (new FakeRelationship)->forceFill(['id' => 1]); + $user2 = (new FakeRelationship)->forceFill(['id' => 2]); // sync user 1 using Model - $post = new Post(); + $post = new Post; $post->author()->associate($user1); $post->syncOriginal(); diff --git a/tests/Integration/Database/EloquentWhereTest.php b/tests/Integration/Database/EloquentWhereTest.php index c7fcc470e6f4..7d46a8e34616 100644 --- a/tests/Integration/Database/EloquentWhereTest.php +++ b/tests/Integration/Database/EloquentWhereTest.php @@ -3,7 +3,10 @@ namespace Illuminate\Tests\Integration\Database; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\MultipleRecordsFoundException; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; /** @@ -91,6 +94,73 @@ public function testFirstWhere() UserWhereTest::firstWhere(['name' => 'wrong-name', 'email' => 'test-email1'], null, null, 'or')) ); } + + public function testSole() + { + $expected = UserWhereTest::create([ + 'name' => 'test-name', + 'email' => 'test-email', + 'address' => 'test-address', + ]); + + $this->assertTrue($expected->is(UserWhereTest::where('name', 'test-name')->sole())); + } + + public function testSoleFailsForMultipleRecords() + { + UserWhereTest::create([ + 'name' => 'test-name', + 'email' => 'test-email', + 'address' => 'test-address', + ]); + + UserWhereTest::create([ + 'name' => 'test-name', + 'email' => 'other-email', + 'address' => 'other-address', + ]); + + $this->expectException(MultipleRecordsFoundException::class); + + UserWhereTest::where('name', 'test-name')->sole(); + } + + public function testSoleFailsIfNoRecords() + { + try { + UserWhereTest::where('name', 'test-name')->sole(); + } catch (ModelNotFoundException $exception) { + // + } + + $this->assertSame(UserWhereTest::class, $exception->getModel()); + } + + public function testChunkMap() + { + UserWhereTest::create([ + 'name' => 'first-name', + 'email' => 'first-email', + 'address' => 'first-address', + ]); + + UserWhereTest::create([ + 'name' => 'second-name', + 'email' => 'second-email', + 'address' => 'second-address', + ]); + + DB::enableQueryLog(); + + $results = UserWhereTest::orderBy('id')->chunkMap(function ($user) { + return $user->name; + }, 1); + + $this->assertCount(2, $results); + $this->assertSame('first-name', $results[0]); + $this->assertSame('second-name', $results[1]); + $this->assertCount(3, DB::getQueryLog()); + } } class UserWhereTest extends Model diff --git a/tests/Integration/Database/MigratorEventsTest.php b/tests/Integration/Database/MigratorEventsTest.php index 75d3ac6327da..0ecbb91827e1 100644 --- a/tests/Integration/Database/MigratorEventsTest.php +++ b/tests/Integration/Database/MigratorEventsTest.php @@ -41,16 +41,16 @@ public function testMigrationEventsContainTheMigrationAndMethod() $this->artisan('migrate:rollback', $this->migrateOptions()); Event::assertDispatched(MigrationStarted::class, function ($event) { - return $event->method == 'up' && $event->migration instanceof Migration; + return $event->method === 'up' && $event->migration instanceof Migration; }); Event::assertDispatched(MigrationStarted::class, function ($event) { - return $event->method == 'down' && $event->migration instanceof Migration; + return $event->method === 'down' && $event->migration instanceof Migration; }); Event::assertDispatched(MigrationEnded::class, function ($event) { - return $event->method == 'up' && $event->migration instanceof Migration; + return $event->method === 'up' && $event->migration instanceof Migration; }); Event::assertDispatched(MigrationEnded::class, function ($event) { - return $event->method == 'down' && $event->migration instanceof Migration; + return $event->method === 'down' && $event->migration instanceof Migration; }); } @@ -62,10 +62,10 @@ public function testTheNoMigrationEventIsFiredWhenNothingToMigrate() $this->artisan('migrate:rollback'); Event::assertDispatched(NoPendingMigrations::class, function ($event) { - return $event->method == 'up'; + return $event->method === 'up'; }); Event::assertDispatched(NoPendingMigrations::class, function ($event) { - return $event->method == 'down'; + return $event->method === 'down'; }); } } diff --git a/tests/Integration/Database/QueryBuilderTest.php b/tests/Integration/Database/QueryBuilderTest.php index f01d91c6ee89..d9257dc6f309 100644 --- a/tests/Integration/Database/QueryBuilderTest.php +++ b/tests/Integration/Database/QueryBuilderTest.php @@ -3,6 +3,8 @@ namespace Illuminate\Tests\Integration\Database; use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Database\MultipleRecordsFoundException; +use Illuminate\Database\RecordsNotFoundException; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -30,6 +32,31 @@ protected function setUp(): void ]); } + public function testSole() + { + $expected = ['id' => '1', 'title' => 'Foo Post']; + + $this->assertSame($expected, (array) DB::table('posts')->where('title', 'Foo Post')->select('id', 'title')->sole()); + } + + public function testSoleFailsForMultipleRecords() + { + DB::table('posts')->insert([ + ['title' => 'Foo Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2017-11-12 13:14:15')], + ]); + + $this->expectException(MultipleRecordsFoundException::class); + + DB::table('posts')->where('title', 'Foo Post')->sole(); + } + + public function testSoleFailsIfNoRecords() + { + $this->expectException(RecordsNotFoundException::class); + + DB::table('posts')->where('title', 'Baz Post')->sole(); + } + public function testSelect() { $expected = ['id' => '1', 'title' => 'Foo Post']; @@ -186,4 +213,18 @@ public function testPaginateWithSpecificColumns() (object) ['title' => 'Bar Post', 'content' => 'Lorem Ipsum.'], ]); } + + public function testChunkMap() + { + DB::enableQueryLog(); + + $results = DB::table('posts')->orderBy('id')->chunkMap(function ($post) { + return $post->title; + }, 1); + + $this->assertCount(2, $results); + $this->assertSame('Foo Post', $results[0]); + $this->assertSame('Bar Post', $results[1]); + $this->assertCount(3, DB::getQueryLog()); + } } diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 8b18d7b17d44..6c27935c7e2d 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -61,9 +61,9 @@ public function testRegisterCustomDoctrineType() 'DROP TABLE __temp__test', ]; - $statements = $blueprint->toSql($this->getConnection(), new SQLiteGrammar()); + $statements = $blueprint->toSql($this->getConnection(), new SQLiteGrammar); - $blueprint->build($this->getConnection(), new SQLiteGrammar()); + $blueprint->build($this->getConnection(), new SQLiteGrammar); $this->assertArrayHasKey(TinyInteger::NAME, Type::getTypesMap()); $this->assertSame('tinyinteger', Schema::getColumnType('test', 'test_column')); diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php index b69b86e8c88e..bf5717b1e694 100644 --- a/tests/Integration/Events/EventFakeTest.php +++ b/tests/Integration/Events/EventFakeTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Events; +use Closure; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Event; @@ -126,6 +127,24 @@ public function testNonFakedHaltedEventGetsProperlyDispatchedAndReturnsResponse( Event::assertNotDispatched(NonImportantEvent::class); } + + public function testAssertListening() + { + Event::fake(); + Event::listen('event', 'listener'); + Event::listen('event', PostEventSubscriber::class); + Event::listen('event', [PostEventSubscriber::class, 'foo']); + Event::subscribe(PostEventSubscriber::class); + Event::listen(function (NonImportantEvent $event) { + // do something + }); + + Event::assertListening('event', 'listener'); + Event::assertListening('event', PostEventSubscriber::class); + Event::assertListening('event', [PostEventSubscriber::class, 'foo']); + Event::assertListening('post-created', [PostEventSubscriber::class, 'handlePostCreated']); + Event::assertListening(NonImportantEvent::class, Closure::class); + } } class Post extends Model @@ -138,6 +157,21 @@ class NonImportantEvent // } +class PostEventSubscriber +{ + public function handlePostCreated($event) + { + } + + public function subscribe($events) + { + $events->listen( + 'post-created', + [PostEventSubscriber::class, 'handlePostCreated'] + ); + } +} + class PostObserver { public function saving(Post $post) diff --git a/tests/Integration/Foundation/MaintenanceModeTest.php b/tests/Integration/Foundation/MaintenanceModeTest.php index 31f08df44da9..ff93fe41a492 100644 --- a/tests/Integration/Foundation/MaintenanceModeTest.php +++ b/tests/Integration/Foundation/MaintenanceModeTest.php @@ -14,7 +14,7 @@ */ class MaintenanceModeTest extends TestCase { - public function tearDown(): void + protected function tearDown(): void { @unlink(storage_path('framework/down')); } diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php index 2553fd49c1e8..a1e5e2cafacf 100644 --- a/tests/Integration/Http/ResourceTest.php +++ b/tests/Integration/Http/ResourceTest.php @@ -781,7 +781,7 @@ public function testCollectionResourcesAreCountable() $collection = new PostCollectionResource($posts); $this->assertCount(2, $collection); - $this->assertSame(2, count($collection)); + $this->assertCount(2, $collection); } public function testKeysArePreservedIfTheResourceIsFlaggedToPreserveKeys() diff --git a/tests/Integration/Mail/SendingMailWithLocaleTest.php b/tests/Integration/Mail/SendingMailWithLocaleTest.php index 951d00a9e157..de1c12ee1008 100644 --- a/tests/Integration/Mail/SendingMailWithLocaleTest.php +++ b/tests/Integration/Mail/SendingMailWithLocaleTest.php @@ -67,7 +67,7 @@ public function testMailIsSentWithSelectedLocale() public function testMailIsSentWithLocaleFromMailable() { - $mailable = new TestMail(); + $mailable = new TestMail; $mailable->locale('ar'); Mail::to('test@mail.com')->send($mailable); diff --git a/tests/Integration/Notifications/SendingMailNotificationsTest.php b/tests/Integration/Notifications/SendingMailNotificationsTest.php index cd46217c373a..0f5d595e859c 100644 --- a/tests/Integration/Notifications/SendingMailNotificationsTest.php +++ b/tests/Integration/Notifications/SendingMailNotificationsTest.php @@ -203,12 +203,12 @@ public function testMailIsSentWithSubject() $user->notify($notification); } - public function testMailIsSentToMultipleAdresses() + public function testMailIsSentToMultipleAddresses() { $notification = new TestMailNotificationWithSubject; $notification->id = Str::uuid()->toString(); - $user = NotifiableUserWithMultipleAddreses::forceCreate([ + $user = NotifiableUserWithMultipleAddresses::forceCreate([ 'email' => 'taylor@laravel.com', ]); @@ -365,7 +365,7 @@ public function routeNotificationForMail($notification) } } -class NotifiableUserWithMultipleAddreses extends NotifiableUser +class NotifiableUserWithMultipleAddresses extends NotifiableUser { public function routeNotificationForMail($notification) { diff --git a/tests/Integration/Queue/CustomPayloadTest.php b/tests/Integration/Queue/CustomPayloadTest.php new file mode 100644 index 000000000000..2ce39544be34 --- /dev/null +++ b/tests/Integration/Queue/CustomPayloadTest.php @@ -0,0 +1,65 @@ +app->make(QueueingDispatcher::class); + + $dispatcher->dispatchToQueue(new MyJob); + } +} + +class QueueServiceProvider extends ServiceProvider +{ + public function register() + { + $this->app->bind('one.time.password', function () { + return random_int(1, 10); + }); + + Queue::createPayloadUsing(function () { + $password = $this->app->make('one.time.password'); + + $this->app->offsetUnset('one.time.password'); + + return ['password' => $password]; + }); + } +} + +class MyJob implements ShouldQueue +{ + public $connection = 'sync'; + + public function handle() + { + // + } +} diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 5f4d143b5b51..337bfcd5b197 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -63,8 +63,8 @@ public function testJobsCanBeChainedOnSuccessUsingPendingChain() public function testJobsCanBeChainedOnSuccessUsingBusFacade() { Bus::dispatchChain([ - new JobChainingTestFirstJob(), - new JobChainingTestSecondJob(), + new JobChainingTestFirstJob, + new JobChainingTestSecondJob, ]); $this->assertTrue(JobChainingTestFirstJob::$ran); @@ -74,8 +74,8 @@ public function testJobsCanBeChainedOnSuccessUsingBusFacade() public function testJobsCanBeChainedOnSuccessUsingBusFacadeAsArguments() { Bus::dispatchChain( - new JobChainingTestFirstJob(), - new JobChainingTestSecondJob() + new JobChainingTestFirstJob, + new JobChainingTestSecondJob ); $this->assertTrue(JobChainingTestFirstJob::$ran); @@ -156,9 +156,9 @@ public function testThirdJobIsNotFiredIfSecondFails() public function testCatchCallbackIsCalledOnFailure() { Bus::chain([ - new JobChainingTestFirstJob(), - new JobChainingTestFailingJob(), - new JobChainingTestSecondJob(), + new JobChainingTestFirstJob, + new JobChainingTestFailingJob, + new JobChainingTestSecondJob, ])->catch(static function () { self::$catchCallbackRan = true; })->dispatch(); diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index 4391c50b54d8..7d7daf8fafe7 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -27,7 +27,7 @@ public function testJobCanUseCustomMethodsAfterDispatch() Job::dispatch('test')->replaceValue('new-test'); $this->assertTrue(Job::$ran); - $this->assertEquals('new-test', Job::$value); + $this->assertSame('new-test', Job::$value); } } diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php index 332442d2cc17..dc82a083c1b3 100644 --- a/tests/Integration/Queue/ModelSerializationTest.php +++ b/tests/Integration/Queue/ModelSerializationTest.php @@ -232,8 +232,8 @@ public function testItSerializesACollectionInCorrectOrder() $unserialized = unserialize($serialized); - $this->assertEquals('taylor@laravel.com', $unserialized->users->first()->email); - $this->assertEquals('mohamed@laravel.com', $unserialized->users->last()->email); + $this->assertSame('taylor@laravel.com', $unserialized->users->first()->email); + $this->assertSame('mohamed@laravel.com', $unserialized->users->last()->email); } public function testItCanUnserializeACollectionInCorrectOrderAndHandleDeletedModels() @@ -252,8 +252,8 @@ public function testItCanUnserializeACollectionInCorrectOrderAndHandleDeletedMod $this->assertCount(2, $unserialized->users); - $this->assertEquals('3@laravel.com', $unserialized->users->first()->email); - $this->assertEquals('1@laravel.com', $unserialized->users->last()->email); + $this->assertSame('3@laravel.com', $unserialized->users->first()->email); + $this->assertSame('1@laravel.com', $unserialized->users->last()->email); } public function testItCanUnserializeCustomCollection() diff --git a/tests/Integration/Queue/QueueConnectionTest.php b/tests/Integration/Queue/QueueConnectionTest.php index d9824e22d077..2a264fce8f34 100644 --- a/tests/Integration/Queue/QueueConnectionTest.php +++ b/tests/Integration/Queue/QueueConnectionTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Bus; use Mockery as m; use Orchestra\Testbench\TestCase; +use Throwable; /** * @group integration @@ -52,7 +53,7 @@ public function testJobWillGetDispatchedInsideATransactionWhenExplicitlyIndicate try { Bus::dispatch((new QueueConnectionTestJob)->beforeCommit()); - } catch (\Throwable $e) { + } catch (Throwable $e) { // This job was dispatched } } diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index d73dd58a2c29..b90104690ef6 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -80,6 +80,22 @@ public function testJobsCanHaveConditionalRateLimits() $this->assertJobWasReleased(NonAdminTestJob::class); } + public function testMiddlewareSerialization() + { + $rateLimited = new RateLimited('limiterName'); + $rateLimited->shouldRelease = false; + + $restoredRateLimited = unserialize(serialize($rateLimited)); + + $fetch = (function (string $name) { + return $this->{$name}; + })->bindTo($restoredRateLimited, RateLimited::class); + + $this->assertFalse($restoredRateLimited->shouldRelease); + $this->assertSame('limiterName', $fetch('limiterName')); + $this->assertInstanceOf(RateLimiter::class, $fetch('limiter')); + } + protected function assertJobRanSuccessfully($class) { $class::$handled = false; diff --git a/tests/Integration/Queue/RateLimitedWithRedisTest.php b/tests/Integration/Queue/RateLimitedWithRedisTest.php index 175f531bbfa0..b8571a91a098 100644 --- a/tests/Integration/Queue/RateLimitedWithRedisTest.php +++ b/tests/Integration/Queue/RateLimitedWithRedisTest.php @@ -7,6 +7,7 @@ use Illuminate\Cache\RateLimiter; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Contracts\Queue\Job; +use Illuminate\Contracts\Redis\Factory as Redis; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Queue\CallQueuedHandler; use Illuminate\Queue\InteractsWithQueue; @@ -111,6 +112,23 @@ public function testJobsCanHaveConditionalRateLimits() $this->assertJobWasReleased($nonAdminJob); } + public function testMiddlewareSerialization() + { + $rateLimited = new RateLimitedWithRedis('limiterName'); + $rateLimited->shouldRelease = false; + + $restoredRateLimited = unserialize(serialize($rateLimited)); + + $fetch = (function (string $name) { + return $this->{$name}; + })->bindTo($restoredRateLimited, RateLimitedWithRedis::class); + + $this->assertFalse($restoredRateLimited->shouldRelease); + $this->assertSame('limiterName', $fetch('limiterName')); + $this->assertInstanceOf(RateLimiter::class, $fetch('limiter')); + $this->assertInstanceOf(Redis::class, $fetch('redis')); + } + protected function assertJobRanSuccessfully($testJob) { $testJob::$handled = false; diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php new file mode 100644 index 000000000000..002acc30c661 --- /dev/null +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -0,0 +1,147 @@ +assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); + } + + public function testCircuitStaysClosedForSuccessfulJobs() + { + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + } + + public function testCircuitResetsAfterSuccess() + { + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); + } + + protected function assertJobWasReleasedImmediately($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->with(0)->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } + + protected function assertJobWasReleasedWithDelay($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->withArgs(function ($delay) { + return $delay >= 600; + })->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertFalse($class::$handled); + } + + protected function assertJobRanSuccessfully($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('isReleased')->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } +} + +class CircuitBreakerTestJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [(new ThrottlesExceptions(2, 10))->by('test')]; + } +} + +class CircuitBreakerSuccessfulJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + } + + public function middleware() + { + return [(new ThrottlesExceptions(2, 10))->by('test')]; + } +} diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php new file mode 100644 index 000000000000..c789d5d523f6 --- /dev/null +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -0,0 +1,167 @@ +setUpRedis(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->tearDownRedis(); + + m::close(); + } + + public function testCircuitIsOpenedForJobErrors() + { + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); + $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); + } + + public function testCircuitStaysClosedForSuccessfulJobs() + { + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key = Str::random()); + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); + } + + public function testCircuitResetsAfterSuccess() + { + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); + $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); + } + + protected function assertJobWasReleasedImmediately($class, $key) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->with(0)->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class($key)), + ]); + + $this->assertTrue($class::$handled); + } + + protected function assertJobWasReleasedWithDelay($class, $key) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->withArgs(function ($delay) { + return $delay >= 600; + })->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class($key)), + ]); + + $this->assertFalse($class::$handled); + } + + protected function assertJobRanSuccessfully($class, $key) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('isReleased')->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + + $instance->call($job, [ + 'command' => serialize($command = new $class($key)), + ]); + + $this->assertTrue($class::$handled); + } +} + +class CircuitBreakerWithRedisTestJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function __construct($key) + { + $this->key = $key; + } + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [(new ThrottlesExceptionsWithRedis(2, 10))->by($this->key)]; + } +} + +class CircuitBreakerWithRedisSuccessfulJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function __construct($key) + { + $this->key = $key; + } + + public function handle() + { + static::$handled = true; + } + + public function middleware() + { + return [(new ThrottlesExceptionsWithRedis(2, 10))->by($this->key)]; + } +} diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index f463eb58881e..4bc207b7fbd7 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Queue; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\ShouldBeUnique; @@ -83,7 +84,7 @@ public function testLockIsReleasedForFailedJobs() { UniqueTestFailJob::$handled = false; - $this->expectException(\Exception::class); + $this->expectException(Exception::class); try { dispatch($job = new UniqueTestFailJob); @@ -191,7 +192,7 @@ public function handle() { static::$handled = true; - throw new \Exception; + throw new Exception; } } diff --git a/tests/Integration/Queue/WithoutOverlappingJobsTest.php b/tests/Integration/Queue/WithoutOverlappingJobsTest.php index c693c338533c..d07ddfae834e 100644 --- a/tests/Integration/Queue/WithoutOverlappingJobsTest.php +++ b/tests/Integration/Queue/WithoutOverlappingJobsTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Queue; +use Exception; use Illuminate\Bus\Dispatcher; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Cache\Repository as Cache; @@ -57,7 +58,7 @@ public function testLockIsReleasedOnJobExceptions() $job->shouldReceive('isReleased')->andReturn(false); $job->shouldReceive('isDeletedOrReleased')->andReturn(false); - $this->expectException(\Exception::class); + $this->expectException(Exception::class); try { $instance->call($job, [ @@ -147,6 +148,6 @@ public function handle() { static::$handled = true; - throw new \Exception; + throw new Exception; } } diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 26ea5921ba40..c41939328fa1 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -119,7 +119,7 @@ public function testMaxJobsExceeded() public function testMaxTimeExceeded() { - Queue::connection('database')->push(new ThirdJob()); + Queue::connection('database')->push(new ThirdJob); Queue::connection('database')->push(new FirstJob); Queue::connection('database')->push(new SecondJob); diff --git a/tests/Integration/Routing/CompiledRouteCollectionTest.php b/tests/Integration/Routing/CompiledRouteCollectionTest.php index 22928a4c1f4f..09edb7d7feba 100644 --- a/tests/Integration/Routing/CompiledRouteCollectionTest.php +++ b/tests/Integration/Routing/CompiledRouteCollectionTest.php @@ -39,8 +39,7 @@ protected function tearDown(): void { parent::tearDown(); - unset($this->routeCollection); - unset($this->router); + unset($this->routeCollection, $this->router); } /** @@ -490,6 +489,59 @@ public function testTrailingSlashIsTrimmedWhenMatchingCachedRoutes() $this->assertSame('foo', $this->collection()->match($request)->getName()); } + public function testRouteWithSamePathAndSameMethodButDiffDomainNameWithOptionsMethod() + { + $routes = [ + 'foo_domain' => $this->newRoute('GET', 'same/path', [ + 'uses' => 'FooController@index', + 'as' => 'foo', + 'domain' => 'foo.localhost', + ]), + 'bar_domain' => $this->newRoute('GET', 'same/path', [ + 'uses' => 'BarController@index', + 'as' => 'bar', + 'domain' => 'bar.localhost', + ]), + 'no_domain' => $this->newRoute('GET', 'same/path', [ + 'uses' => 'BarController@index', + 'as' => 'no_domain', + ]), + ]; + + $this->routeCollection->add($routes['foo_domain']); + $this->routeCollection->add($routes['bar_domain']); + $this->routeCollection->add($routes['no_domain']); + + $expectedMethods = [ + 'OPTIONS', + ]; + + $this->assertSame($expectedMethods, $this->collection()->match( + Request::create('http://foo.localhost/same/path', 'OPTIONS') + )->methods); + + $this->assertSame($expectedMethods, $this->collection()->match( + Request::create('http://bar.localhost/same/path', 'OPTIONS') + )->methods); + + $this->assertSame($expectedMethods, $this->collection()->match( + Request::create('http://no.localhost/same/path', 'OPTIONS') + )->methods); + + $this->assertEquals([ + 'HEAD' => [ + 'foo.localhost/same/path' => $routes['foo_domain'], + 'bar.localhost/same/path' => $routes['bar_domain'], + 'same/path' => $routes['no_domain'], + ], + 'GET' => [ + 'foo.localhost/same/path' => $routes['foo_domain'], + 'bar.localhost/same/path' => $routes['bar_domain'], + 'same/path' => $routes['no_domain'], + ], + ], $this->collection()->getRoutesByMethod()); + } + /** * Create a new Route object. * diff --git a/tests/Integration/Testing/ArtisanCommandTest.php b/tests/Integration/Testing/ArtisanCommandTest.php new file mode 100644 index 000000000000..82e827732597 --- /dev/null +++ b/tests/Integration/Testing/ArtisanCommandTest.php @@ -0,0 +1,132 @@ +ask('What is your name?'); + + $language = $this->choice('Which language do you prefer?', [ + 'PHP', + 'Ruby', + 'Python', + ]); + + $this->line("Your name is $name and you prefer $language."); + }); + + Artisan::command('slim', function () { + $this->line($this->ask('Who?')); + $this->line($this->ask('What?')); + $this->line($this->ask('Huh?')); + }); + } + + public function test_console_command_that_passes() + { + $this->artisan('survey') + ->expectsQuestion('What is your name?', 'Taylor Otwell') + ->expectsQuestion('Which language do you prefer?', 'PHP') + ->expectsOutput('Your name is Taylor Otwell and you prefer PHP.') + ->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.') + ->assertExitCode(0); + } + + public function test_console_command_that_passes_with_repeating_output() + { + $this->artisan('slim') + ->expectsQuestion('Who?', 'Taylor') + ->expectsQuestion('What?', 'Taylor') + ->expectsQuestion('Huh?', 'Taylor') + ->expectsOutput('Taylor') + ->doesntExpectOutput('Otwell') + ->expectsOutput('Taylor') + ->expectsOutput('Taylor') + ->assertExitCode(0); + } + + public function test_console_command_that_fails_from_unexpected_output() + { + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Output "Your name is Taylor Otwell and you prefer PHP." was printed.'); + + $this->artisan('survey') + ->expectsQuestion('What is your name?', 'Taylor Otwell') + ->expectsQuestion('Which language do you prefer?', 'PHP') + ->doesntExpectOutput('Your name is Taylor Otwell and you prefer PHP.') + ->assertExitCode(0); + } + + public function test_console_command_that_fails_from_missing_output() + { + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Output "Your name is Taylor Otwell and you prefer PHP." was not printed.'); + + $this->ignoringMockOnceExceptions(function () { + $this->artisan('survey') + ->expectsQuestion('What is your name?', 'Taylor Otwell') + ->expectsQuestion('Which language do you prefer?', 'Ruby') + ->expectsOutput('Your name is Taylor Otwell and you prefer PHP.') + ->assertExitCode(0); + }); + } + + public function test_console_command_that_fails_from_exit_code_mismatch() + { + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Expected status code 1 but received 0.'); + + $this->artisan('survey') + ->expectsQuestion('What is your name?', 'Taylor Otwell') + ->expectsQuestion('Which language do you prefer?', 'PHP') + ->assertExitCode(1); + } + + public function test_console_command_that_fails_from_unordered_output() + { + $this->expectException(InvalidOrderException::class); + + $this->ignoringMockOnceExceptions(function () { + $this->artisan('slim') + ->expectsQuestion('Who?', 'Taylor') + ->expectsQuestion('What?', 'Danger') + ->expectsQuestion('Huh?', 'Otwell') + ->expectsOutput('Taylor') + ->expectsOutput('Otwell') + ->expectsOutput('Danger') + ->assertExitCode(0); + }); + } + + /** + * Don't allow Mockery's InvalidCountException to be reported. Mocks setup + * in PendingCommand cause PHPUnit tearDown() to later throw the exception. + * + * @param callable $callback + * @return void + */ + protected function ignoringMockOnceExceptions(callable $callback) + { + try { + $callback(); + } finally { + try { + Mockery::close(); + } catch (InvalidCountException $e) { + // Ignore mock exception from PendingCommand::expectsOutput(). + } + } + } +} diff --git a/tests/Integration/View/BladeTest.php b/tests/Integration/View/BladeTest.php index a26460ba90ec..b3d57f8f7776 100644 --- a/tests/Integration/View/BladeTest.php +++ b/tests/Integration/View/BladeTest.php @@ -42,12 +42,18 @@ public function test_rendering_the_same_dynamic_component_with_different_attribu $this->assertSame(' Hello Taylor - - + Hello Samuel ', trim($view)); } + public function test_inline_link_type_attributes_dont_add_extra_spacing_at_end() + { + $view = View::make('uses-link')->render(); + + $this->assertSame('This is a sentence with a link.', trim($view)); + } + public function test_appendable_attributes() { $view = View::make('uses-appendable-panel', ['name' => 'Taylor', 'withInjectedValue' => true])->render(); @@ -63,6 +69,13 @@ public function test_appendable_attributes() ', trim($view)); } + public function tested_nested_anonymous_attribute_proxying_works_correctly() + { + $view = View::make('uses-child-input')->render(); + + $this->assertSame('', trim($view)); + } + protected function getEnvironmentSetUp($app) { $app['config']->set('view.paths', [__DIR__.'/templates']); diff --git a/tests/Integration/View/RenderableViewExceptionTest.php b/tests/Integration/View/RenderableViewExceptionTest.php new file mode 100644 index 000000000000..93c91cb31387 --- /dev/null +++ b/tests/Integration/View/RenderableViewExceptionTest.php @@ -0,0 +1,36 @@ +get('/'); + + $response->assertSee('This is a renderable exception.'); + } + + protected function getEnvironmentSetUp($app) + { + $app['config']->set('view.paths', [__DIR__.'/templates']); + } +} + +class RenderableException extends Exception +{ + public function render($request) + { + return new Response('This is a renderable exception.'); + } +} diff --git a/tests/Integration/View/templates/components/base-input.blade.php b/tests/Integration/View/templates/components/base-input.blade.php new file mode 100644 index 000000000000..4d8f09367c55 --- /dev/null +++ b/tests/Integration/View/templates/components/base-input.blade.php @@ -0,0 +1,11 @@ +@props(['disabled' => false]) + +@php +if ($disabled) { + $class = 'disabled-class'; +} else { + $class = 'not-disabled-class'; +} +@endphp + +merge(['class' => $class]) }} {{ $disabled ? 'disabled' : '' }} /> \ No newline at end of file diff --git a/tests/Integration/View/templates/components/child-input.blade.php b/tests/Integration/View/templates/components/child-input.blade.php new file mode 100644 index 000000000000..711e20d4e613 --- /dev/null +++ b/tests/Integration/View/templates/components/child-input.blade.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Integration/View/templates/components/link.blade.php b/tests/Integration/View/templates/components/link.blade.php new file mode 100644 index 000000000000..74b8835393b6 --- /dev/null +++ b/tests/Integration/View/templates/components/link.blade.php @@ -0,0 +1,3 @@ +@props(['href']) + +{{ $slot }} \ No newline at end of file diff --git a/tests/Integration/View/templates/renderable-exception.blade.php b/tests/Integration/View/templates/renderable-exception.blade.php new file mode 100644 index 000000000000..28649eefa7f9 --- /dev/null +++ b/tests/Integration/View/templates/renderable-exception.blade.php @@ -0,0 +1,3 @@ +@php + throw new Illuminate\Tests\Integration\View\RenderableException; +@endphp diff --git a/tests/Integration/View/templates/uses-child-input.blade.php b/tests/Integration/View/templates/uses-child-input.blade.php new file mode 100644 index 000000000000..bc5bdade5d89 --- /dev/null +++ b/tests/Integration/View/templates/uses-child-input.blade.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Integration/View/templates/uses-link.blade.php b/tests/Integration/View/templates/uses-link.blade.php new file mode 100644 index 000000000000..53a67cf64e30 --- /dev/null +++ b/tests/Integration/View/templates/uses-link.blade.php @@ -0,0 +1 @@ +This is a sentence with a link. \ No newline at end of file diff --git a/tests/Log/LogManagerTest.php b/tests/Log/LogManagerTest.php index f365c9f701c0..65cd162d76ad 100755 --- a/tests/Log/LogManagerTest.php +++ b/tests/Log/LogManagerTest.php @@ -327,7 +327,7 @@ public function testLogManagerCreateSyslogDriverWithConfiguredFormatter() $this->assertSame('Y/m/d--test', $dateFormat->getValue($formatter)); } - public function testLogMnagerPurgeResolvedChannels() + public function testLogManagerPurgeResolvedChannels() { $manager = new LogManager($this->app); diff --git a/tests/Mail/MailLogTransportTest.php b/tests/Mail/MailLogTransportTest.php index 644ee64c5827..5848734d2eec 100644 --- a/tests/Mail/MailLogTransportTest.php +++ b/tests/Mail/MailLogTransportTest.php @@ -36,7 +36,7 @@ public function testGetLogTransportWithConfiguredChannel() public function testGetLogTransportWithPsrLogger() { $this->app['config']->set('mail.driver', 'log'); - $logger = $this->app->instance('log', new NullLogger()); + $logger = $this->app->instance('log', new NullLogger); $transportLogger = app('mailer')->getSwiftMailer()->getTransport()->logger(); diff --git a/tests/Mail/MailManagerTest.php b/tests/Mail/MailManagerTest.php index 9a67d4c2d9ca..e7610ffe1cdd 100644 --- a/tests/Mail/MailManagerTest.php +++ b/tests/Mail/MailManagerTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Mail; +use InvalidArgumentException; use Orchestra\Testbench\TestCase; class MailManagerTest extends TestCase @@ -21,7 +22,7 @@ public function testEmptyTransportConfig($transport) 'timeout' => null, ]); - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("Unsupported mail transport [{$transport}]"); $this->app['mail.manager']->mailer('custom_smtp'); } diff --git a/tests/Mail/MailMarkdownTest.php b/tests/Mail/MailMarkdownTest.php index 7878174ac43b..19341fde67a0 100644 --- a/tests/Mail/MailMarkdownTest.php +++ b/tests/Mail/MailMarkdownTest.php @@ -20,7 +20,7 @@ public function testRenderFunctionReturnsHtml() $markdown = new Markdown($viewFactory); $viewFactory->shouldReceive('flushFinderCache')->once(); $viewFactory->shouldReceive('replaceNamespace')->once()->with('mail', $markdown->htmlComponentPaths())->andReturnSelf(); - $viewFactory->shouldReceive('exists')->with('default')->andReturn(false); + $viewFactory->shouldReceive('exists')->with('mail.default')->andReturn(false); $viewFactory->shouldReceive('make')->with('view', [])->andReturnSelf(); $viewFactory->shouldReceive('make')->with('mail::themes.default', [])->andReturnSelf(); $viewFactory->shouldReceive('render')->twice()->andReturn('', 'body {}'); @@ -37,9 +37,26 @@ public function testRenderFunctionReturnsHtmlWithCustomTheme() $markdown->theme('yaz'); $viewFactory->shouldReceive('flushFinderCache')->once(); $viewFactory->shouldReceive('replaceNamespace')->once()->with('mail', $markdown->htmlComponentPaths())->andReturnSelf(); - $viewFactory->shouldReceive('exists')->with('yaz')->andReturn(true); + $viewFactory->shouldReceive('exists')->with('mail.yaz')->andReturn(true); $viewFactory->shouldReceive('make')->with('view', [])->andReturnSelf(); - $viewFactory->shouldReceive('make')->with('yaz', [])->andReturnSelf(); + $viewFactory->shouldReceive('make')->with('mail.yaz', [])->andReturnSelf(); + $viewFactory->shouldReceive('render')->twice()->andReturn('', 'body {}'); + + $result = $markdown->render('view', []); + + $this->assertNotFalse(strpos($result, '')); + } + + public function testRenderFunctionReturnsHtmlWithCustomThemeWithMailPrefix() + { + $viewFactory = m::mock(Factory::class); + $markdown = new Markdown($viewFactory); + $markdown->theme('mail.yaz'); + $viewFactory->shouldReceive('flushFinderCache')->once(); + $viewFactory->shouldReceive('replaceNamespace')->once()->with('mail', $markdown->htmlComponentPaths())->andReturnSelf(); + $viewFactory->shouldReceive('exists')->with('mail.yaz')->andReturn(true); + $viewFactory->shouldReceive('make')->with('view', [])->andReturnSelf(); + $viewFactory->shouldReceive('make')->with('mail.yaz', [])->andReturnSelf(); $viewFactory->shouldReceive('render')->twice()->andReturn('', 'body {}'); $result = $markdown->render('view', []); diff --git a/tests/Mail/MailSesTransportTest.php b/tests/Mail/MailSesTransportTest.php index ff787fb7b53b..5d1d8f1fe885 100644 --- a/tests/Mail/MailSesTransportTest.php +++ b/tests/Mail/MailSesTransportTest.php @@ -16,7 +16,7 @@ class MailSesTransportTest extends TestCase /** @group Foo */ public function testGetTransport() { - $container = new Container(); + $container = new Container; $container->singleton('config', function () { return new Repository([ diff --git a/tests/Mail/MailableQueuedTest.php b/tests/Mail/MailableQueuedTest.php index 99854f82ce81..47b93429e62e 100644 --- a/tests/Mail/MailableQueuedTest.php +++ b/tests/Mail/MailableQueuedTest.php @@ -32,7 +32,7 @@ public function testQueuedMailableSent() ->onlyMethods(['createMessage', 'to']) ->getMock(); $mailer->setQueue($queueFake); - $mailable = new MailableQueableStub; + $mailable = new MailableQueueableStub; $queueFake->assertNothingPushed(); $mailer->send($mailable); $queueFake->assertPushedOn(null, SendQueuedMailable::class); @@ -46,7 +46,7 @@ public function testQueuedMailableWithAttachmentSent() ->onlyMethods(['createMessage']) ->getMock(); $mailer->setQueue($queueFake); - $mailable = new MailableQueableStub; + $mailable = new MailableQueueableStub; $attachmentOption = ['mime' => 'image/jpeg', 'as' => 'bar.jpg']; $mailable->attach('foo.jpg', $attachmentOption); $this->assertIsArray($mailable->attachments); @@ -75,7 +75,7 @@ public function testQueuedMailableWithAttachmentFromDiskSent() ->onlyMethods(['createMessage']) ->getMock(); $mailer->setQueue($queueFake); - $mailable = new MailableQueableStub; + $mailable = new MailableQueueableStub; $attachmentOption = ['mime' => 'image/jpeg', 'as' => 'bar.jpg']; $mailable->attachFromStorage('/', 'foo.jpg', $attachmentOption); @@ -95,7 +95,7 @@ protected function getMocks() } } -class MailableQueableStub extends Mailable implements ShouldQueue +class MailableQueueableStub extends Mailable implements ShouldQueue { use Queueable; diff --git a/tests/Notifications/NotificationBroadcastChannelTest.php b/tests/Notifications/NotificationBroadcastChannelTest.php index e4cae95c3219..51c9401671fa 100644 --- a/tests/Notifications/NotificationBroadcastChannelTest.php +++ b/tests/Notifications/NotificationBroadcastChannelTest.php @@ -83,7 +83,7 @@ public function testNotificationIsBroadcastedNow() $events = m::mock(Dispatcher::class); $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) { - return $event->connection == 'sync'; + return $event->connection === 'sync'; })); $channel = new BroadcastChannel($events); $channel->send($notifiable, $notification); diff --git a/tests/Notifications/NotificationMailMessageTest.php b/tests/Notifications/NotificationMailMessageTest.php index f86949a5a369..ba31b96df353 100644 --- a/tests/Notifications/NotificationMailMessageTest.php +++ b/tests/Notifications/NotificationMailMessageTest.php @@ -141,11 +141,11 @@ public function testWhenCallback() $mailMessage->cc('cc@example.com'); }; - $message = new MailMessage(); + $message = new MailMessage; $message->when(true, $callback); $this->assertSame([['cc@example.com', null]], $message->cc); - $message = new MailMessage(); + $message = new MailMessage; $message->when(false, $callback); $this->assertSame([], $message->cc); } @@ -158,12 +158,12 @@ public function testWhenCallbackWithReturn() return $mailMessage->cc('cc@example.com'); }; - $message = new MailMessage(); + $message = new MailMessage; $message->when(true, $callback)->bcc('bcc@example.com'); $this->assertSame([['cc@example.com', null]], $message->cc); $this->assertSame([['bcc@example.com', null]], $message->bcc); - $message = new MailMessage(); + $message = new MailMessage; $message->when(false, $callback)->bcc('bcc@example.com'); $this->assertSame([], $message->cc); $this->assertSame([['bcc@example.com', null]], $message->bcc); @@ -172,7 +172,7 @@ public function testWhenCallbackWithReturn() public function testWhenCallbackWithDefault() { $callback = function (MailMessage $mailMessage, $condition) { - $this->assertEquals('truthy', $condition); + $this->assertSame('truthy', $condition); $mailMessage->cc('truthy@example.com'); }; @@ -183,11 +183,11 @@ public function testWhenCallbackWithDefault() $mailMessage->cc('zero@example.com'); }; - $message = new MailMessage(); + $message = new MailMessage; $message->when('truthy', $callback, $default); $this->assertSame([['truthy@example.com', null]], $message->cc); - $message = new MailMessage(); + $message = new MailMessage; $message->when(0, $callback, $default); $this->assertSame([['zero@example.com', null]], $message->cc); } @@ -200,11 +200,11 @@ public function testUnlessCallback() $mailMessage->cc('test@example.com'); }; - $message = new MailMessage(); + $message = new MailMessage; $message->unless(false, $callback); $this->assertSame([['test@example.com', null]], $message->cc); - $message = new MailMessage(); + $message = new MailMessage; $message->unless(true, $callback); $this->assertSame([], $message->cc); } @@ -217,12 +217,12 @@ public function testUnlessCallbackWithReturn() return $mailMessage->cc('cc@example.com'); }; - $message = new MailMessage(); + $message = new MailMessage; $message->unless(false, $callback)->bcc('bcc@example.com'); $this->assertSame([['cc@example.com', null]], $message->cc); $this->assertSame([['bcc@example.com', null]], $message->bcc); - $message = new MailMessage(); + $message = new MailMessage; $message->unless(true, $callback)->bcc('bcc@example.com'); $this->assertSame([], $message->cc); $this->assertSame([['bcc@example.com', null]], $message->bcc); @@ -237,16 +237,16 @@ public function testUnlessCallbackWithDefault() }; $default = function (MailMessage $mailMessage, $condition) { - $this->assertEquals('truthy', $condition); + $this->assertSame('truthy', $condition); $mailMessage->cc('truthy@example.com'); }; - $message = new MailMessage(); + $message = new MailMessage; $message->unless(0, $callback, $default); $this->assertSame([['zero@example.com', null]], $message->cc); - $message = new MailMessage(); + $message = new MailMessage; $message->unless('truthy', $callback, $default); $this->assertSame([['truthy@example.com', null]], $message->cc); } diff --git a/tests/Notifications/NotificationSendQueuedNotificationTest.php b/tests/Notifications/NotificationSendQueuedNotificationTest.php index bdd1f5e197a6..3068d8130fe4 100644 --- a/tests/Notifications/NotificationSendQueuedNotificationTest.php +++ b/tests/Notifications/NotificationSendQueuedNotificationTest.php @@ -36,7 +36,7 @@ public function testSerializationOfNotifiableModel() $identifier = new ModelIdentifier(NotifiableUser::class, [null], [], null); $serializedIdentifier = serialize($identifier); - $job = new SendQueuedNotifications(new NotifiableUser(), 'notification'); + $job = new SendQueuedNotifications(new NotifiableUser, 'notification'); $serialized = serialize($job); $this->assertStringContainsString($serializedIdentifier, $serialized); @@ -44,7 +44,7 @@ public function testSerializationOfNotifiableModel() public function testSerializationOfNormalNotifiable() { - $notifiable = new AnonymousNotifiable(); + $notifiable = new AnonymousNotifiable; $serializedNotifiable = serialize($notifiable); $job = new SendQueuedNotifications($notifiable, 'notification'); diff --git a/tests/Notifications/NotificationSenderTest.php b/tests/Notifications/NotificationSenderTest.php index 6c5a6aaf849d..5c8674a45db8 100644 --- a/tests/Notifications/NotificationSenderTest.php +++ b/tests/Notifications/NotificationSenderTest.php @@ -33,7 +33,7 @@ public function testItCanSendQueuedNotificationsWithAStringVia() $sender = new NotificationSender($manager, $bus, $events); - $sender->send($notifiable, new DummyQueuedNotificationWithStringVia()); + $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); } public function testItCanSendNotificationsWithAnEmptyStringVia() @@ -46,7 +46,7 @@ public function testItCanSendNotificationsWithAnEmptyStringVia() $sender = new NotificationSender($manager, $bus, $events); - $sender->sendNow($notifiable, new DummyNotificationWithEmptyStringVia()); + $sender->sendNow($notifiable, new DummyNotificationWithEmptyStringVia); } public function testItCannotSendNotificationsViaDatabaseForAnonymousNotifiables() @@ -59,7 +59,7 @@ public function testItCannotSendNotificationsViaDatabaseForAnonymousNotifiables( $sender = new NotificationSender($manager, $bus, $events); - $sender->sendNow($notifiable, new DummyNotificationWithDatabaseVia()); + $sender->sendNow($notifiable, new DummyNotificationWithDatabaseVia); } } diff --git a/tests/Pipeline/PipelineTest.php b/tests/Pipeline/PipelineTest.php index da7038fb9ef5..f057e2c765f6 100644 --- a/tests/Pipeline/PipelineTest.php +++ b/tests/Pipeline/PipelineTest.php @@ -28,8 +28,7 @@ public function testPipelineBasicUsage() $this->assertSame('foo', $_SERVER['__test.pipe.one']); $this->assertSame('foo', $_SERVER['__test.pipe.two']); - unset($_SERVER['__test.pipe.one']); - unset($_SERVER['__test.pipe.two']); + unset($_SERVER['__test.pipe.one'], $_SERVER['__test.pipe.two']); } public function testPipelineUsageWithObjects() diff --git a/tests/Queue/QueueBeanstalkdQueueTest.php b/tests/Queue/QueueBeanstalkdQueueTest.php index 7134917a2369..534a16141381 100755 --- a/tests/Queue/QueueBeanstalkdQueueTest.php +++ b/tests/Queue/QueueBeanstalkdQueueTest.php @@ -13,6 +13,16 @@ class QueueBeanstalkdQueueTest extends TestCase { + /** + * @var BeanstalkdQueue + */ + private $queue; + + /** + * @var Container|m\LegacyMockInterface|m\MockInterface + */ + private $container; + protected function tearDown(): void { m::close(); @@ -26,14 +36,16 @@ public function testPushProperlyPushesJobOntoBeanstalkd() return $uuid; }); - $queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), 'default', 60); - $pheanstalk = $queue->getPheanstalk(); + $this->setQueue('default', 60); + $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('useTube')->once()->with('stack')->andReturn($pheanstalk); $pheanstalk->shouldReceive('useTube')->once()->with('default')->andReturn($pheanstalk); $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), 1024, 0, 60); - $queue->push('foo', ['data'], 'stack'); - $queue->push('foo', ['data']); + $this->queue->push('foo', ['data'], 'stack'); + $this->queue->push('foo', ['data']); + + $this->container->shouldHaveReceived('bound')->with('events')->times(2); Str::createUuidsNormally(); } @@ -46,53 +58,68 @@ public function testDelayedPushProperlyPushesJobOntoBeanstalkd() return $uuid; }); - $queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), 'default', 60); - $pheanstalk = $queue->getPheanstalk(); + $this->setQueue('default', 60); + $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('useTube')->once()->with('stack')->andReturn($pheanstalk); $pheanstalk->shouldReceive('useTube')->once()->with('default')->andReturn($pheanstalk); $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR); - $queue->later(5, 'foo', ['data'], 'stack'); - $queue->later(5, 'foo', ['data']); + $this->queue->later(5, 'foo', ['data'], 'stack'); + $this->queue->later(5, 'foo', ['data']); + + $this->container->shouldHaveReceived('bound')->with('events')->times(2); Str::createUuidsNormally(); } public function testPopProperlyPopsJobOffOfBeanstalkd() { - $queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), 'default', 60); - $queue->setContainer(m::mock(Container::class)); - $pheanstalk = $queue->getPheanstalk(); + $this->setQueue('default', 60); + + $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('watchOnly')->once()->with('default')->andReturn($pheanstalk); $job = m::mock(Job::class); $pheanstalk->shouldReceive('reserveWithTimeout')->once()->with(0)->andReturn($job); - $result = $queue->pop(); + $result = $this->queue->pop(); $this->assertInstanceOf(BeanstalkdJob::class, $result); } public function testBlockingPopProperlyPopsJobOffOfBeanstalkd() { - $queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), 'default', 60, 60); - $queue->setContainer(m::mock(Container::class)); - $pheanstalk = $queue->getPheanstalk(); + $this->setQueue('default', 60, 60); + + $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('watchOnly')->once()->with('default')->andReturn($pheanstalk); $job = m::mock(Job::class); $pheanstalk->shouldReceive('reserveWithTimeout')->once()->with(60)->andReturn($job); - $result = $queue->pop(); + $result = $this->queue->pop(); $this->assertInstanceOf(BeanstalkdJob::class, $result); } public function testDeleteProperlyRemoveJobsOffBeanstalkd() { - $queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), 'default', 60); - $pheanstalk = $queue->getPheanstalk(); + $this->setQueue('default', 60); + + $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('useTube')->once()->with('default')->andReturn($pheanstalk); $pheanstalk->shouldReceive('delete')->once()->with(m::type(Job::class)); - $queue->deleteMessage('default', 1); + $this->queue->deleteMessage('default', 1); + } + + /** + * @param string $default + * @param int $timeToRun + * @param int $blockFor + */ + private function setQueue($default, $timeToRun, $blockFor = 0) + { + $this->queue = new BeanstalkdQueue(m::mock(Pheanstalk::class), $default, $timeToRun, $blockFor); + $this->container = m::spy(Container::class); + $this->queue->setContainer($this->container); } } diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 6fa35eca5708..c87dc754545b 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Queue; +use Illuminate\Container\Container; use Illuminate\Database\Connection; use Illuminate\Queue\DatabaseQueue; use Illuminate\Queue\Queue; @@ -28,6 +29,7 @@ public function testPushProperlyPushesJobOntoDatabase() $queue = $this->getMockBuilder(DatabaseQueue::class)->onlyMethods(['currentTime'])->setConstructorArgs([$database = m::mock(Connection::class), 'table', 'default'])->getMock(); $queue->expects($this->any())->method('currentTime')->willReturn('time'); + $queue->setContainer($container = m::spy(Container::class)); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) use ($uuid) { $this->assertSame('default', $array['queue']); @@ -39,6 +41,8 @@ public function testPushProperlyPushesJobOntoDatabase() $queue->push('foo', ['data']); + $container->shouldHaveReceived('bound')->with('events')->once(); + Str::createUuidsNormally(); } @@ -56,6 +60,7 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() [$database = m::mock(Connection::class), 'table', 'default'] )->getMock(); $queue->expects($this->any())->method('currentTime')->willReturn('time'); + $queue->setContainer($container = m::spy(Container::class)); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) use ($uuid) { $this->assertSame('default', $array['queue']); @@ -67,6 +72,8 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() $queue->later(10, 'foo', ['data']); + $container->shouldHaveReceived('bound')->with('events')->once(); + Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 2060772d78a8..952384b7f200 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Queue; +use Illuminate\Container\Container; use Illuminate\Contracts\Redis\Factory; use Illuminate\Queue\LuaScripts; use Illuminate\Queue\Queue; @@ -28,11 +29,13 @@ public function testPushProperlyPushesJobOntoRedis() $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); + $queue->setContainer($container = m::spy(Container::class)); $redis->shouldReceive('connection')->once()->andReturn($redis); $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'id' => 'foo', 'attempts' => 0])); $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); + $container->shouldHaveReceived('bound')->with('events')->once(); Str::createUuidsNormally(); } @@ -47,6 +50,7 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook() $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); + $queue->setContainer($container = m::spy(Container::class)); $redis->shouldReceive('connection')->once()->andReturn($redis); $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'custom' => 'taylor', 'id' => 'foo', 'attempts' => 0])); @@ -56,6 +60,7 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook() $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); + $container->shouldHaveReceived('bound')->with('events')->once(); Queue::createPayloadUsing(null); @@ -72,6 +77,7 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook() $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); + $queue->setContainer($container = m::spy(Container::class)); $redis->shouldReceive('connection')->once()->andReturn($redis); $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'custom' => 'taylor', 'bar' => 'foo', 'id' => 'foo', 'attempts' => 0])); @@ -85,6 +91,7 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook() $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); + $container->shouldHaveReceived('bound')->with('events')->once(); Queue::createPayloadUsing(null); @@ -100,6 +107,7 @@ public function testDelayedPushProperlyPushesJobOntoRedis() }); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['availableAt', 'getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); + $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->expects($this->once())->method('availableAt')->with(1)->willReturn(2); @@ -112,6 +120,7 @@ public function testDelayedPushProperlyPushesJobOntoRedis() $id = $queue->later(1, 'foo', ['data']); $this->assertSame('foo', $id); + $container->shouldHaveReceived('bound')->with('events')->once(); Str::createUuidsNormally(); } @@ -126,6 +135,7 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis() $date = Carbon::now(); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['availableAt', 'getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); + $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->expects($this->once())->method('availableAt')->with($date)->willReturn(2); @@ -137,6 +147,7 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis() ); $queue->later($date, 'foo', ['data']); + $container->shouldHaveReceived('bound')->with('events')->once(); Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 789084515b0d..60e02b161ebb 100755 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -92,33 +92,39 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoSqs() { $now = Carbon::now(); $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'secondsUntil', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->queueName, $this->mockedData)->willReturn($this->mockedPayload); $queue->expects($this->once())->method('secondsUntil')->with($now)->willReturn(5); $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); $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(); } public function testDelayedPushProperlyPushesJobOntoSqs() { $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'secondsUntil', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->queueName, $this->mockedData)->willReturn($this->mockedPayload); $queue->expects($this->once())->method('secondsUntil')->with($this->mockedDelay)->willReturn($this->mockedDelay); $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); $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(); } public function testPushProperlyPushesJobOntoSqs() { $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['createPayload', 'getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); + $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('createPayload')->with($this->mockedJob, $this->queueName, $this->mockedData)->willReturn($this->mockedPayload); $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); $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(); } public function testSizeProperlyReadsSqsQueueSize() diff --git a/tests/Queue/RedisQueueIntegrationTest.php b/tests/Queue/RedisQueueIntegrationTest.php index 0380988bd4e1..5fbad9311dd4 100644 --- a/tests/Queue/RedisQueueIntegrationTest.php +++ b/tests/Queue/RedisQueueIntegrationTest.php @@ -3,7 +3,9 @@ namespace Illuminate\Tests\Queue; use Illuminate\Container\Container; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; +use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Jobs\RedisJob; use Illuminate\Queue\RedisQueue; use Illuminate\Support\Carbon; @@ -21,6 +23,11 @@ class RedisQueueIntegrationTest extends TestCase */ private $queue; + /** + * @var \Mockery\MockInterface|\Mockery\LegacyMockInterface + */ + private $container; + protected function setUp(): void { Carbon::setTestNow(Carbon::now()); @@ -57,6 +64,8 @@ public function testExpiredJobsArePopped($driver) $this->queue->later(-300, $jobs[2]); $this->queue->later(-100, $jobs[3]); + $this->container->shouldHaveReceived('bound')->with('events')->times(4); + $this->assertEquals($jobs[2], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertEquals($jobs[1], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); $this->assertEquals($jobs[3], unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); @@ -183,13 +192,14 @@ public function testPopProperlyPopsDelayedJobOffOfRedis($driver) */ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) { - $this->queue = new RedisQueue($this->redis[$driver], 'default', null, null); - $this->queue->setContainer(m::mock(Container::class)); + $this->setQueue($driver, 'default', null, null); // Push an item into queue $job = new RedisQueueIntegrationTestJob(10); $this->queue->later(-10, $job); + $this->container->shouldHaveReceived('bound')->with('events')->once(); + // Pop and check it is popped correctly $before = $this->currentTime(); $this->assertEquals($job, unserialize(json_decode($this->queue->pop()->getRawBody())->data->command)); @@ -264,12 +274,13 @@ public function testBlockingPopProperlyPopsExpiredJobs($driver) */ public function testNotExpireJobsWhenExpireNull($driver) { - $this->queue = new RedisQueue($this->redis[$driver], 'default', null, null); - $this->queue->setContainer(m::mock(Container::class)); + $this->setQueue($driver, 'default', null, null); // Make an expired reserved job $failed = new RedisQueueIntegrationTestJob(-20); $this->queue->push($failed); + $this->container->shouldHaveReceived('bound')->with('events')->once(); + $beforeFailPop = $this->currentTime(); $this->queue->pop(); $afterFailPop = $this->currentTime(); @@ -277,6 +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); // Pop and check it is popped correctly $before = $this->currentTime(); @@ -309,12 +321,12 @@ public function testNotExpireJobsWhenExpireNull($driver) */ public function testExpireJobsWhenExpireSet($driver) { - $this->queue = new RedisQueue($this->redis[$driver], 'default', null, 30); - $this->queue->setContainer(m::mock(Container::class)); + $this->setQueue($driver, 'default', null, 30); // Push an item into queue $job = new RedisQueueIntegrationTestJob(10); $this->queue->push($job); + $this->container->shouldHaveReceived('bound')->with('events')->once(); // Pop and check it is popped correctly $before = $this->currentTime(); @@ -455,17 +467,67 @@ public function testSize($driver) $this->assertEquals(2, $this->queue->size()); } + /** + * @dataProvider redisDriverProvider + * + * @param string $driver + */ + public function testPushJobQueuedEvent($driver) + { + $events = m::mock(Dispatcher::class); + $events->shouldReceive('dispatch')->withArgs(function (JobQueued $jobQueued) { + $this->assertInstanceOf(RedisQueueIntegrationTestJob::class, $jobQueued->job); + $this->assertIsString(RedisQueueIntegrationTestJob::class, $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(); + + $queue = new RedisQueue($this->redis[$driver]); + $queue->setContainer($container); + + $queue->push(new RedisQueueIntegrationTestJob(5)); + } + + /** + * @dataProvider redisDriverProvider + * + * @param string $driver + */ + public function testBulkJobQueuedEvent($driver) + { + $events = m::mock(Dispatcher::class); + $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); + + $queue = new RedisQueue($this->redis[$driver]); + $queue->setContainer($container); + + $queue->bulk([ + new RedisQueueIntegrationTestJob(5), + new RedisQueueIntegrationTestJob(10), + new RedisQueueIntegrationTestJob(15), + ]); + } + /** * @param string $driver * @param string $default - * @param string $connection + * @param string|null $connection * @param int $retryAfter * @param int|null $blockFor */ private function setQueue($driver, $default = 'default', $connection = null, $retryAfter = 60, $blockFor = null) { $this->queue = new RedisQueue($this->redis[$driver], $default, $connection, $retryAfter, $blockFor); - $this->queue->setContainer(m::mock(Container::class)); + $this->container = m::spy(Container::class); + $this->queue->setContainer($this->container); } } diff --git a/tests/Redis/ConcurrentLimiterTest.php b/tests/Redis/ConcurrentLimiterTest.php index 35e5e64d2ea0..7285e9586732 100644 --- a/tests/Redis/ConcurrentLimiterTest.php +++ b/tests/Redis/ConcurrentLimiterTest.php @@ -149,7 +149,7 @@ public function testItReleasesIfErrorIsThrown() try { $lock->block(1, function () { - throw new Error(); + throw new Error; }); } catch (Error $e) { } diff --git a/tests/Redis/RedisConnectionTest.php b/tests/Redis/RedisConnectionTest.php index 0ca1ddae5452..38ab3fb7452b 100644 --- a/tests/Redis/RedisConnectionTest.php +++ b/tests/Redis/RedisConnectionTest.php @@ -698,6 +698,33 @@ public function testItSscansForKeys() } } + public function testItSPopsForKeys() + { + foreach ($this->connections() as $redis) { + $members = ['test:spop:1', 'test:spop:2', 'test:spop:3', 'test:spop:4']; + + foreach ($members as $member) { + $redis->sadd('set', $member); + } + + $result = $redis->spop('set'); + $this->assertIsNotArray($result); + $this->assertContains($result, $members); + + $result = $redis->spop('set', 1); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + + $result = $redis->spop('set', 2); + + $this->assertIsArray($result); + $this->assertCount(2, $result); + + $redis->flushAll(); + } + } + public function testPhpRedisScanOption() { foreach ($this->connections() as $redis) { diff --git a/tests/Redis/RedisConnectorTest.php b/tests/Redis/RedisConnectorTest.php index ff5f93470e8d..07ec786fe2fd 100644 --- a/tests/Redis/RedisConnectorTest.php +++ b/tests/Redis/RedisConnectorTest.php @@ -41,7 +41,7 @@ public function testDefaultConfiguration() $phpRedisClient = $this->redis['phpredis']->connection()->client(); $this->assertEquals($host, $phpRedisClient->getHost()); $this->assertEquals($port, $phpRedisClient->getPort()); - $this->assertEquals('default', $phpRedisClient->client('GETNAME')); + $this->assertSame('default', $phpRedisClient->client('GETNAME')); } public function testUrl() @@ -161,4 +161,27 @@ public function testScheme() $this->assertSame("tcp://{$host}", $phpRedisClient->getHost()); $this->assertEquals($port, $phpRedisClient->getPort()); } + + public function testPredisConfigurationWithUsername() + { + $host = env('REDIS_HOST', '127.0.0.1'); + $port = env('REDIS_PORT', 6379); + $username = 'testuser'; + $password = 'testpw'; + + $predis = new RedisManager(new Application, 'predis', [ + 'default' => [ + 'host' => $host, + 'port' => $port, + 'username' => $username, + 'password' => $password, + 'database' => 5, + 'timeout' => 0.5, + ], + ]); + $predisClient = $predis->connection()->client(); + $parameters = $predisClient->getConnection()->getParameters(); + $this->assertEquals($username, $parameters->username); + $this->assertEquals($password, $parameters->password); + } } diff --git a/tests/Redis/RedisManagerExtensionTest.php b/tests/Redis/RedisManagerExtensionTest.php index 04b7eb63d7d9..5cac41877cc2 100644 --- a/tests/Redis/RedisManagerExtensionTest.php +++ b/tests/Redis/RedisManagerExtensionTest.php @@ -19,7 +19,7 @@ protected function setUp(): void { parent::setUp(); - $this->redis = new RedisManager(new Application(), 'my_custom_driver', [ + $this->redis = new RedisManager(new Application, 'my_custom_driver', [ 'default' => [ 'host' => 'some-host', 'port' => 'some-port', @@ -39,7 +39,7 @@ protected function setUp(): void ]); $this->redis->extend('my_custom_driver', function () { - return new FakeRedisConnnector(); + return new FakeRedisConnector; }); } @@ -72,7 +72,7 @@ public function test_parse_connection_configuration_for_cluster() 'url3', ], ]; - $redis = new RedisManager(new Application(), 'my_custom_driver', [ + $redis = new RedisManager(new Application, 'my_custom_driver', [ 'clusters' => [ $name => $config, ], @@ -91,7 +91,7 @@ public function test_parse_connection_configuration_for_cluster() } } -class FakeRedisConnnector implements Connector +class FakeRedisConnector implements Connector { /** * Create a new clustered Predis connection. diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index 47fcb2aa31e5..9802ad742a61 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -334,6 +334,24 @@ public function testCanRegisterResourcesWithoutOption() } } + public function testCanRegisterResourceWithMissingOption() + { + $this->router->middleware('resource-middleware') + ->resource('users', RouteRegistrarControllerStub::class) + ->missing(function () { + return 'missing'; + }); + + $this->assertIsCallable($this->router->getRoutes()->getByName('users.show')->getMissing()); + $this->assertIsCallable($this->router->getRoutes()->getByName('users.edit')->getMissing()); + $this->assertIsCallable($this->router->getRoutes()->getByName('users.update')->getMissing()); + $this->assertIsCallable($this->router->getRoutes()->getByName('users.destroy')->getMissing()); + + $this->assertNull($this->router->getRoutes()->getByName('users.index')->getMissing()); + $this->assertNull($this->router->getRoutes()->getByName('users.create')->getMissing()); + $this->assertNull($this->router->getRoutes()->getByName('users.store')->getMissing()); + } + public function testCanAccessRegisteredResourceRoutesAsRouteCollection() { $resource = $this->router->middleware('resource-middleware') diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 6715aa7297d9..a0d4548dc06e 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -15,6 +15,7 @@ use Illuminate\Events\Dispatcher; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Routing\Controller; @@ -1671,6 +1672,27 @@ public function testImplicitBindingsWithOptionalParameterWithExistingKeyInUri() $this->assertSame('taylor', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); } + public function testImplicitBindingsWithMissingModelHandledByMissing() + { + $router = $this->getRouter(); + $router->get('foo/{bar}', [ + 'middleware' => SubstituteBindings::class, + 'uses' => function (RouteModelBindingNullStub $bar = null) { + $this->assertInstanceOf(RouteModelBindingNullStub::class, $bar); + + return $bar->first(); + }, + ])->missing(function () { + return new RedirectResponse('/', 302); + }); + + $request = Request::create('foo/taylor', 'GET'); + + $response = $router->dispatch($request); + $this->assertTrue($response->isRedirect('/')); + $this->assertEquals(302, $response->getStatusCode()); + } + public function testImplicitBindingsWithOptionalParameterWithNoKeyInUri() { $router = $this->getRouter(); @@ -1831,7 +1853,7 @@ public function testRouteRedirectStripsMissingStartingForwardSlash() public function testRouteRedirectExceptionWhenMissingExpectedParameters() { $this->expectException(UrlGenerationException::class); - $this->expectExceptionMessage('Missing required parameters for [Route: laravel_route_redirect_destination] [URI: users/{user}].'); + $this->expectExceptionMessage('Missing required parameter for [Route: laravel_route_redirect_destination] [URI: users/{user}] [Missing parameter: user].'); $container = new Container; $router = new Router(new Dispatcher, $container); diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index b7663b686b17..676f609b15fa 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -550,6 +550,61 @@ public function testUrlGenerationForControllersRequiresPassingOfRequiredParamete $this->assertSame('http://www.foo.com:8080/foo?test=123', $url->route('foo', $parameters)); } + public function provideParametersAndExpectedMeaningfulExceptionMessages() + { + return [ + 'Missing parameters "one", "two" and "three"' => [ + [], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, two, three].', + ], + 'Missing parameters "two" and "three"' => [ + ['one' => '123'], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: two, three].', + ], + 'Missing parameters "one" and "three"' => [ + ['two' => '123'], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, three].', + ], + 'Missing parameters "one" and "two"' => [ + ['three' => '123'], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, two].', + ], + 'Missing parameter "three"' => [ + ['one' => '123', 'two' => '123'], + 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: three].', + ], + 'Missing parameter "two"' => [ + ['one' => '123', 'three' => '123'], + 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: two].', + ], + 'Missing parameter "one"' => [ + ['two' => '123', 'three' => '123'], + 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: one].', + ], + ]; + } + + /** + * @dataProvider provideParametersAndExpectedMeaningfulExceptionMessages + */ + public function testUrlGenerationThrowsExceptionForMissingParametersWithMeaningfulMessage($parameters, $expectedMeaningfulExceptionMessage) + { + $this->expectException(UrlGenerationException::class); + $this->expectExceptionMessage($expectedMeaningfulExceptionMessage); + + $url = new UrlGenerator( + $routes = new RouteCollection, + Request::create('http://www.foo.com:8080/') + ); + + $route = new Route(['GET'], 'foo/{one}/{two}/{three}/{four?}', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + $url->route('foo', $parameters); + } + public function testForceRootUrl() { $url = new UrlGenerator( diff --git a/tests/Support/SupportCarbonTest.php b/tests/Support/SupportCarbonTest.php index ea4a53bfff91..cdd865b8b470 100644 --- a/tests/Support/SupportCarbonTest.php +++ b/tests/Support/SupportCarbonTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use Carbon\Carbon as BaseCarbon; +use Carbon\CarbonImmutable as BaseCarbonImmutable; use DateTime; use DateTimeInterface; use Illuminate\Support\Carbon; @@ -108,4 +109,13 @@ public function testDeserializationOccursCorrectly() $this->assertInstanceOf(Carbon::class, $deserialized); } + + public function testSetTestNowWillPersistBetweenImmutableAndMutableInstance() + { + Carbon::setTestNow(new Carbon('2017-06-27 13:14:15.000000')); + + $this->assertSame('2017-06-27 13:14:15', Carbon::now()->toDateTimeString()); + $this->assertSame('2017-06-27 13:14:15', BaseCarbon::now()->toDateTimeString()); + $this->assertSame('2017-06-27 13:14:15', BaseCarbonImmutable::now()->toDateTimeString()); + } } diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 6faf65a1a3c2..3c09a0018def 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -78,8 +78,8 @@ public function testFirstWhere($collection) $this->assertSame('book', $data->firstWhere('material', 'paper')['type']); $this->assertSame('gasket', $data->firstWhere('material', 'rubber')['type']); - $this->assertNull($data->firstWhere('material', 'nonexistant')); - $this->assertNull($data->firstWhere('nonexistant', 'key')); + $this->assertNull($data->firstWhere('material', 'nonexistent')); + $this->assertNull($data->firstWhere('nonexistent', 'key')); } /** @@ -541,6 +541,16 @@ public function testCountableByWithCallback($collection) })->all()); } + /** + * @dataProvider collectionClassProvider + */ + public function testContainsOneItem($collection) + { + $this->assertFalse((new $collection([]))->containsOneItem()); + $this->assertTrue((new $collection([1]))->containsOneItem()); + $this->assertFalse((new $collection([1, 2]))->containsOneItem()); + } + public function testIterable() { $c = new Collection(['foo']); @@ -572,7 +582,7 @@ public function testFilter($collection) $c = new $collection(['id' => 1, 'first' => 'Hello', 'second' => 'World']); $this->assertEquals(['first' => 'Hello', 'second' => 'World'], $c->filter(function ($item, $key) { - return $key != 'id'; + return $key !== 'id'; })->all()); } @@ -766,8 +776,10 @@ public function testWhereStrict($collection) */ public function testWhereInstanceOf($collection) { - $c = new $collection([new stdClass, new stdClass, new $collection, new stdClass]); + $c = new $collection([new stdClass, new stdClass, new $collection, new stdClass, new Str]); $this->assertCount(3, $c->whereInstanceOf(stdClass::class)); + + $this->assertCount(4, $c->whereInstanceOf([stdClass::class, Str::class])); } /** @@ -3114,7 +3126,7 @@ public function testRejectRemovesElementsPassingTruthTest($collection) $c = new $collection(['foo', 'bar']); $this->assertEquals(['foo'], $c->reject(function ($v) { - return $v == 'bar'; + return $v === 'bar'; })->values()->all()); $c = new $collection(['foo', null]); @@ -3125,12 +3137,12 @@ public function testRejectRemovesElementsPassingTruthTest($collection) $c = new $collection(['foo', 'bar']); $this->assertEquals(['foo', 'bar'], $c->reject(function ($v) { - return $v == 'baz'; + return $v === 'baz'; })->values()->all()); $c = new $collection(['id' => 1, 'primary' => 'foo', 'secondary' => 'bar']); $this->assertEquals(['primary' => 'foo', 'secondary' => 'bar'], $c->reject(function ($item, $key) { - return $key == 'id'; + return $key === 'id'; })->all()); } @@ -3203,7 +3215,7 @@ public function testSearchReturnsFalseWhenItemIsNotFound($collection) return $value < 1 && is_numeric($value); })); $this->assertFalse($c->search(function ($value) { - return $value == 'nope'; + return $value === 'nope'; })); } @@ -3568,7 +3580,7 @@ public function testConcatWithCollection($collection) */ public function testDump($collection) { - $log = new Collection(); + $log = new Collection; VarDumper::setHandler(function ($value) use ($log) { $log->add($value); @@ -3590,6 +3602,28 @@ public function testReduce($collection) $this->assertEquals(6, $data->reduce(function ($carry, $element) { return $carry += $element; })); + + $data = new $collection([ + 'foo' => 'bar', + 'baz' => 'qux', + ]); + $this->assertSame('foobarbazqux', $data->reduce(function ($carry, $element, $key) { + return $carry .= $key.$element; + })); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testReduceWithKeys($collection) + { + $data = new $collection([ + 'foo' => 'bar', + 'baz' => 'qux', + ]); + $this->assertSame('foobarbazqux', $data->reduceWithKeys(function ($carry, $element, $key) { + return $carry .= $key.$element; + })); } /** diff --git a/tests/Support/SupportFacadesEventTest.php b/tests/Support/SupportFacadesEventTest.php index 8c62c0826ba1..15c4a4acc7ab 100644 --- a/tests/Support/SupportFacadesEventTest.php +++ b/tests/Support/SupportFacadesEventTest.php @@ -38,6 +38,7 @@ protected function setUp(): void protected function tearDown(): void { Event::clearResolvedInstances(); + Event::setFacadeApplication(null); m::close(); } diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index bb3e5ead59ce..c286809fbe23 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Support\Env; use Illuminate\Support\Optional; +use LogicException; use Mockery as m; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -39,6 +40,9 @@ public function testValue() $this->assertSame('foo', value(function () { return 'foo'; })); + $this->assertSame('foo', value(function ($arg) { + return $arg; + }, 'foo')); } public function testObjectGet() @@ -362,10 +366,63 @@ public function testTap() } public function testThrow() + { + $this->expectException(LogicException::class); + + throw_if(true, new LogicException); + } + + public function testThrowDefaultException() + { + $this->expectException(RuntimeException::class); + + throw_if(true); + } + + public function testThrowExceptionWithMessage() { $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('test'); + + throw_if(true, 'test'); + } + + public function testThrowExceptionAsStringWithMessage() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('test'); + + throw_if(true, LogicException::class, 'test'); + } + + public function testThrowUnless() + { + $this->expectException(LogicException::class); + + throw_unless(false, new LogicException); + } + + public function testThrowUnlessDefaultException() + { + $this->expectException(RuntimeException::class); + + throw_unless(false); + } + + public function testThrowUnlessExceptionWithMessage() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('test'); + + throw_unless(false, 'test'); + } + + public function testThrowUnlessExceptionAsStringWithMessage() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('test'); - throw_if(true, new RuntimeException); + throw_unless(false, LogicException::class, 'test'); } public function testThrowReturnIfNotThrown() diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index b61df7a6b632..f6f16803e073 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -484,6 +484,13 @@ public function testIsNotEmptyIsLazy() }); } + public function testContainsOneItemIsLazy() + { + $this->assertEnumerates(2, function ($collection) { + $collection->containsOneItem(); + }); + } + public function testJoinIsLazy() { $this->assertEnumeratesOnce(function ($collection) { diff --git a/tests/Support/SupportLazyCollectionTest.php b/tests/Support/SupportLazyCollectionTest.php index c671830029e1..cbb509e54f2d 100644 --- a/tests/Support/SupportLazyCollectionTest.php +++ b/tests/Support/SupportLazyCollectionTest.php @@ -69,6 +69,31 @@ public function testCanCreateCollectionFromClosure() ], $data->all()); } + public function testCanCreateCollectionFromGenerator() + { + $iterable = function () { + yield 1; + yield 2; + yield 3; + }; + $data = LazyCollection::make($iterable()); + + $this->assertSame([1, 2, 3], $data->all()); + + $iterable = function () { + yield 'a' => 1; + yield 'b' => 2; + yield 'c' => 3; + }; + $data = LazyCollection::make($iterable()); + + $this->assertSame([ + 'a' => 1, + 'b' => 2, + 'c' => 3, + ], $data->all()); + } + public function testEager() { $source = [1, 2, 3, 4, 5]; diff --git a/tests/Support/SupportPluralizerTest.php b/tests/Support/SupportPluralizerTest.php index 5e4f4298ccef..ee3af6e45c04 100755 --- a/tests/Support/SupportPluralizerTest.php +++ b/tests/Support/SupportPluralizerTest.php @@ -16,6 +16,8 @@ public function testBasicPlural() { $this->assertSame('children', Str::plural('child')); $this->assertSame('cod', Str::plural('cod')); + $this->assertSame('The words', Str::plural('The word')); + $this->assertSame('Bouquetés', Str::plural('Bouqueté')); } public function testCaseSensitiveSingularUsage() @@ -68,6 +70,21 @@ public function testPluralStudlyWithCount() $this->assertPluralStudly('RealHumans', 'RealHuman', -2); } + public function testPluralNotAppliedForStringEndingWithNonAlphanumericCharacter() + { + $this->assertSame('Alien.', Str::plural('Alien.')); + $this->assertSame('Alien!', Str::plural('Alien!')); + $this->assertSame('Alien ', Str::plural('Alien ')); + $this->assertSame('50%', Str::plural('50%')); + } + + public function testPluralAppliedForStringEndingWithNumericCharacter() + { + $this->assertSame('User1s', Str::plural('User1')); + $this->assertSame('User2s', Str::plural('User2')); + $this->assertSame('User3s', Str::plural('User3')); + } + private function assertPluralStudly($expected, $value, $count = 2) { $this->assertSame($expected, Str::pluralStudly($value, $count)); diff --git a/tests/Support/SupportReflectorTest.php b/tests/Support/SupportReflectorTest.php index df5b3e414e46..deebed5aabc7 100644 --- a/tests/Support/SupportReflectorTest.php +++ b/tests/Support/SupportReflectorTest.php @@ -81,6 +81,7 @@ class B extends A { public function f(parent $x) { + // } } @@ -92,6 +93,7 @@ class C { public function f(A|Model $x) { + // } }' ); @@ -101,6 +103,7 @@ class TestClassWithCall { public function __call($method, $parameters) { + // } } @@ -108,5 +111,6 @@ class TestClassWithCallStatic { public static function __callStatic($method, $parameters) { + // } } diff --git a/tests/Support/SupportReflectsClosuresTest.php b/tests/Support/SupportReflectsClosuresTest.php index 3ab1200fedd0..1546b33c3696 100644 --- a/tests/Support/SupportReflectsClosuresTest.php +++ b/tests/Support/SupportReflectsClosuresTest.php @@ -12,7 +12,7 @@ public function testReflectsClosures() { $this->assertParameterTypes([ExampleParameter::class], function (ExampleParameter $one) { // assert the Closure isn't actually executed - throw new RuntimeException(); + throw new RuntimeException; }); $this->assertParameterTypes([], function () { diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 87bc3c0956c4..dc8ec88b5f57 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -365,6 +365,20 @@ public function testReplaceLast() $this->assertSame('Malmö Jönköping', Str::replaceLast('', 'yyy', 'Malmö Jönköping')); } + public function testRemove() + { + $this->assertSame('Fbar', Str::remove('o', 'Foobar')); + $this->assertSame('Foo', Str::remove('bar', 'Foobar')); + $this->assertSame('oobar', Str::remove('F', 'Foobar')); + $this->assertSame('Foobar', Str::remove('f', 'Foobar')); + $this->assertSame('oobar', Str::remove('f', 'Foobar', false)); + + $this->assertSame('Fbr', Str::remove(['o', 'a'], 'Foobar')); + $this->assertSame('Fooar', Str::remove(['f', 'b'], 'Foobar')); + $this->assertSame('ooar', Str::remove(['f', 'b'], 'Foobar', false)); + $this->assertSame('Foobar', Str::remove(['f', '|'], 'Foo|bar')); + } + public function testSnake() { $this->assertSame('laravel_p_h_p_framework', Str::snake('LaravelPHPFramework')); @@ -511,6 +525,12 @@ public function invalidUuidList() ['ff6f8cb0-c57da-51e1-9b21-0800200c9a66'], ]; } + + public function testMarkdown() + { + $this->assertSame("

hello world

\n", Str::markdown('*hello world*')); + $this->assertSame("

hello world

\n", Str::markdown('# hello world')); + } } class StringableObjectStub diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 1986d050de9c..857d6402da2c 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -62,6 +62,14 @@ public function testMatch() $this->assertTrue($stringable->matchAll('/nothing/')->isEmpty()); } + public function testTest() + { + $stringable = $this->stringable('foo bar'); + + $this->assertTrue($stringable->test('/bar/')); + $this->assertTrue($stringable->test('/foo (.*)/')); + } + public function testTrim() { $this->assertSame('foo', (string) $this->stringable(' foo ')->trim()); @@ -444,6 +452,20 @@ public function testReplaceLast() $this->assertSame('Malmö Jönköping', (string) $this->stringable('Malmö Jönköping')->replaceLast('', 'yyy')); } + public function testRemove() + { + $this->assertSame('Fbar', (string) $this->stringable('Foobar')->remove('o')); + $this->assertSame('Foo', (string) $this->stringable('Foobar')->remove('bar')); + $this->assertSame('oobar', (string) $this->stringable('Foobar')->remove('F')); + $this->assertSame('Foobar', (string) $this->stringable('Foobar')->remove('f')); + $this->assertSame('oobar', (string) $this->stringable('Foobar')->remove('f', false)); + + $this->assertSame('Fbr', (string) $this->stringable('Foobar')->remove(['o', 'a'])); + $this->assertSame('Fooar', (string) $this->stringable('Foobar')->remove(['f', 'b'])); + $this->assertSame('ooar', (string) $this->stringable('Foobar')->remove(['f', 'b'], false)); + $this->assertSame('Foobar', (string) $this->stringable('Foo|bar')->remove(['f', '|'])); + } + public function testSnake() { $this->assertSame('laravel_p_h_p_framework', (string) $this->stringable('LaravelPHPFramework')->snake()); @@ -545,4 +567,39 @@ public function testChunk() $this->assertInstanceOf(Collection::class, $chunks); $this->assertSame(['foo', 'bar', 'baz'], $chunks->all()); } + + public function testJsonSerialize() + { + $this->assertSame('"foo"', json_encode($this->stringable('foo'))); + } + + public function testTap() + { + $stringable = $this->stringable('foobarbaz'); + + $fromTheTap = ''; + + $stringable = $stringable->tap(function (Stringable $string) use (&$fromTheTap) { + $fromTheTap = $string->substr(0, 3); + }); + + $this->assertSame('foo', (string) $fromTheTap); + $this->assertSame('foobarbaz', (string) $stringable); + } + + public function testPipe() + { + $callback = function ($stringable) { + return 'bar'; + }; + + $this->assertInstanceOf(Stringable::class, $this->stringable('foo')->pipe($callback)); + $this->assertSame('bar', (string) $this->stringable('foo')->pipe($callback)); + } + + public function testMarkdown() + { + $this->assertEquals("

hello world

\n", $this->stringable('*hello world*')->markdown()); + $this->assertEquals("

hello world

\n", $this->stringable('# hello world')->markdown()); + } } diff --git a/tests/Support/SupportTestingEventFakeTest.php b/tests/Support/SupportTestingEventFakeTest.php index 3a9ccc6cdd3b..d51562d10c58 100644 --- a/tests/Support/SupportTestingEventFakeTest.php +++ b/tests/Support/SupportTestingEventFakeTest.php @@ -118,6 +118,21 @@ function ($event, $payload) { $fake->assertDispatched('Bar'); $fake->assertNotDispatched('Baz'); } + + public function testAssertNothingDispatched() + { + $this->fake->assertNothingDispatched(); + + $this->fake->dispatch(EventStub::class); + $this->fake->dispatch(EventStub::class); + + try { + $this->fake->assertNothingDispatched(); + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertThat($e, new ExceptionMessage('2 unexpected events were dispatched.')); + } + } } class EventStub diff --git a/tests/Support/SupportTestingQueueFakeTest.php b/tests/Support/SupportTestingQueueFakeTest.php index a2996c492d2b..cf22717cf29b 100644 --- a/tests/Support/SupportTestingQueueFakeTest.php +++ b/tests/Support/SupportTestingQueueFakeTest.php @@ -220,7 +220,7 @@ public function testAssertPushedWithChainUsingCallback() $this->fake->assertPushedWithChain(JobWithChainAndParameterStub::class, [ JobStub::class, ], function ($job) { - return $job->parameter == 'second'; + return $job->parameter === 'second'; }); try { @@ -228,7 +228,7 @@ public function testAssertPushedWithChainUsingCallback() JobStub::class, JobStub::class, ], function ($job) { - return $job->parameter == 'second'; + return $job->parameter === 'second'; }); $this->fail(); } catch (ExpectationFailedException $e) { diff --git a/tests/Testing/Concerns/TestDatabasesTest.php b/tests/Testing/Concerns/TestDatabasesTest.php new file mode 100644 index 000000000000..7042bd5362c3 --- /dev/null +++ b/tests/Testing/Concerns/TestDatabasesTest.php @@ -0,0 +1,110 @@ +singleton('config', function () { + return m::mock(Config::class) + ->shouldReceive('get') + ->once() + ->with('database.default', null) + ->andReturn('mysql') + ->getMock(); + }); + + $_SERVER['LARAVEL_PARALLEL_TESTING'] = 1; + } + + public function testSwitchToDatabaseWithoutUrl() + { + DB::shouldReceive('purge')->once(); + + config()->shouldReceive('get') + ->once() + ->with('database.connections.mysql.url', false) + ->andReturn(false); + + config()->shouldReceive('set') + ->once() + ->with('database.connections.mysql.database', 'my_database_test_1'); + + $this->switchToDatabase('my_database_test_1'); + } + + /** + * @dataProvider databaseUrls + */ + public function testSwitchToDatabaseWithUrl($testDatabase, $url, $testUrl) + { + DB::shouldReceive('purge')->once(); + + config()->shouldReceive('get') + ->once() + ->with('database.connections.mysql.url', false) + ->andReturn($url); + + config()->shouldReceive('set') + ->once() + ->with('database.connections.mysql.url', $testUrl); + + $this->switchToDatabase($testDatabase); + } + + public function switchToDatabase($database) + { + $instance = new class { + use TestDatabases; + }; + + $method = new ReflectionMethod($instance, 'switchToDatabase'); + tap($method)->setAccessible(true)->invoke($instance, $database); + } + + public function databaseUrls() + { + return [ + [ + 'my_database_test_1', + 'mysql://root:@127.0.0.1/my_database?charset=utf8mb4', + 'mysql://root:@127.0.0.1/my_database_test_1?charset=utf8mb4', + ], + [ + 'my_database_test_1', + 'mysql://my-user:@localhost/my_database', + 'mysql://my-user:@localhost/my_database_test_1', + ], + [ + 'my-database_test_1', + 'postgresql://my_database_user:@127.0.0.1/my-database?charset=utf8', + 'postgresql://my_database_user:@127.0.0.1/my-database_test_1?charset=utf8', + ], + ]; + } + + public function tearDown(): void + { + parent::tearDown(); + + Container::setInstance(null); + DB::clearResolvedInstances(); + DB::setFacadeApplication(null); + + unset($_SERVER['LARAVEL_PARALLEL_TESTING']); + + m::close(); + } +} diff --git a/tests/Testing/Fluent/AssertTest.php b/tests/Testing/Fluent/AssertTest.php new file mode 100644 index 000000000000..69f9278a6956 --- /dev/null +++ b/tests/Testing/Fluent/AssertTest.php @@ -0,0 +1,968 @@ + 'value', + ]); + + $assert->has('prop'); + } + + public function testAssertHasFailsWhenPropMissing() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [prop] does not exist.'); + + $assert->has('prop'); + } + + public function testAssertHasNestedProp() + { + $assert = AssertableJson::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $assert->has('example.nested'); + } + + public function testAssertHasFailsWhenNestedPropMissing() + { + $assert = AssertableJson::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [example.another] does not exist.'); + + $assert->has('example.another'); + } + + public function testAssertHasCountItemsInProp() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $assert->has('bar', 2); + } + + public function testAssertHasCountFailsWhenAmountOfItemsDoesNotMatch() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 1); + } + + public function testAssertHasCountFailsWhenPropMissing() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->has('baz', 1); + } + + public function testAssertHasFailsWhenSecondArgumentUnsupportedType() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'baz', + ]); + + $this->expectException(TypeError::class); + + $assert->has('bar', 'invalid'); + } + + public function testAssertHasOnlyCounts() + { + $assert = AssertableJson::fromArray([ + 'foo', + 'bar', + 'baz', + ]); + + $assert->has(3); + } + + public function testAssertHasOnlyCountFails() + { + $assert = AssertableJson::fromArray([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Root level does not have the expected size.'); + + $assert->has(2); + } + + public function testAssertHasOnlyCountFailsScoped() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', function ($bar) { + $bar->has(3); + }); + } + + public function testAssertCount() + { + $assert = AssertableJson::fromArray([ + 'foo', + 'bar', + 'baz', + ]); + + $assert->count(3); + } + + public function testAssertCountFails() + { + $assert = AssertableJson::fromArray([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Root level does not have the expected size.'); + + $assert->count(2); + } + + public function testAssertCountFailsScoped() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', function ($bar) { + $bar->count(3); + }); + } + + public function testAssertMissing() + { + $assert = AssertableJson::fromArray([ + 'foo' => [ + 'bar' => true, + ], + ]); + + $assert->missing('foo.baz'); + } + + public function testAssertMissingFailsWhenPropExists() + { + $assert = AssertableJson::fromArray([ + 'prop' => 'value', + 'foo' => [ + 'bar' => true, + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.bar] was found while it was expected to be missing.'); + + $assert->missing('foo.bar'); + } + + public function testAssertMissingAll() + { + $assert = AssertableJson::fromArray([ + 'baz' => 'foo', + ]); + + $assert->missingAll([ + 'foo', + 'bar', + ]); + } + + public function testAssertMissingAllFailsWhenAtLeastOnePropExists() + { + $assert = AssertableJson::fromArray([ + 'baz' => 'foo', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was found while it was expected to be missing.'); + + $assert->missingAll([ + 'bar', + 'baz', + ]); + } + + public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() + { + $assert = AssertableJson::fromArray([ + 'baz' => 'foo', + ]); + + $assert->missingAll('foo', 'bar'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was found while it was expected to be missing.'); + + $assert->missingAll('bar', 'baz'); + } + + public function testAssertWhereMatchesValue() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'value', + ]); + + $assert->where('bar', 'value'); + } + + public function testAssertWhereFailsWhenDoesNotMatchValue() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not match the expected value.'); + + $assert->where('bar', 'invalid'); + } + + public function testAssertWhereFailsWhenMissing() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->where('baz', 'invalid'); + } + + public function testAssertWhereFailsWhenMachingLoosely() + { + $assert = AssertableJson::fromArray([ + 'bar' => 1, + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not match the expected value.'); + + $assert->where('bar', true); + } + + public function testAssertWhereUsingClosure() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'baz', + ]); + + $assert->where('bar', function ($value) { + return $value === 'baz'; + }); + } + + public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] was marked as invalid using a closure.'); + + $assert->where('bar', function ($value) { + return $value === 'invalid'; + }); + } + + public function testAssertWhereClosureArrayValuesAreAutomaticallyCastedToCollections() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'foo', + 'example' => 'value', + ], + ]); + + $assert->where('bar', function ($value) { + $this->assertInstanceOf(Collection::class, $value); + + return $value->count() === 2; + }); + } + + public function testAssertWhereMatchesValueUsingArrayable() + { + $stub = ArrayableStubObject::make(['foo' => 'bar']); + + $assert = AssertableJson::fromArray([ + 'bar' => $stub->toArray(), + ]); + + $assert->where('bar', $stub); + } + + public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'foo', + 'example' => 'value', + ], + ]); + + $assert->where('bar', function ($value) { + $this->assertInstanceOf(Collection::class, $value); + + return $value->count() === 2; + }); + } + + public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() + { + $assert = AssertableJson::fromArray([ + 'bar' => ['id' => 1, 'name' => 'Example'], + 'baz' => [ + 'id' => 1, + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'email_verified_at' => '2021-01-22T10:34:42.000000Z', + 'created_at' => '2021-01-22T10:34:42.000000Z', + 'updated_at' => '2021-01-22T10:34:42.000000Z', + ], + ]); + + $assert + ->where('bar', ArrayableStubObject::make(['name' => 'Example', 'id' => 1])) + ->where('baz', [ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'id' => 1, + 'email_verified_at' => '2021-01-22T10:34:42.000000Z', + 'updated_at' => '2021-01-22T10:34:42.000000Z', + 'created_at' => '2021-01-22T10:34:42.000000Z', + ]); + } + + public function testAssertNestedWhereMatchesValue() + { + $assert = AssertableJson::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $assert->where('example.nested', 'nested-value'); + } + + public function testAssertNestedWhereFailsWhenDoesNotMatchValue() + { + $assert = AssertableJson::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [example.nested] does not match the expected value.'); + + $assert->where('example.nested', 'another-value'); + } + + public function testScope() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $called = false; + $assert->has('bar', function (AssertableJson $assert) use (&$called) { + $called = true; + $assert + ->where('baz', 'example') + ->where('prop', 'value'); + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } + + public function testScopeFailsWhenPropMissing() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->has('baz', function (AssertableJson $item) { + $item->where('baz', 'example'); + }); + } + + public function testScopeFailsWhenPropSingleValue() + { + $assert = AssertableJson::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] is not scopeable.'); + + $assert->has('bar', function (AssertableJson $item) { + // + }); + } + + public function testScopeShorthand() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $called = false; + $assert->has('bar', 2, function (AssertableJson $item) use (&$called) { + $item->where('key', 'first'); + $called = true; + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } + + public function testScopeShorthandFailsWhenAssertingZeroItems() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 0, function (AssertableJson $item) { + $item->where('key', 'first'); + }); + } + + public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 1, function (AssertableJson $item) { + $item->where('key', 'first'); + }); + } + + public function testFirstScope() + { + $assert = AssertableJson::fromArray([ + 'foo' => [ + 'key' => 'first', + ], + 'bar' => [ + 'key' => 'second', + ], + ]); + + $assert->first(function (AssertableJson $item) { + $item->where('key', 'first'); + }); + } + + public function testFirstScopeFailsWhenNoProps() + { + $assert = AssertableJson::fromArray([]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Cannot scope directly onto the first element of the root level because it is empty.'); + + $assert->first(function (AssertableJson $item) { + // + }); + } + + public function testFirstNestedScopeFailsWhenNoProps() + { + $assert = AssertableJson::fromArray([ + 'foo' => [], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Cannot scope directly onto the first element of property [foo] because it is empty.'); + + $assert->has('foo', function (AssertableJson $assert) { + $assert->first(function (AssertableJson $item) { + // + }); + }); + } + + public function testFirstScopeFailsWhenPropSingleValue() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo] is not scopeable.'); + + $assert->first(function (AssertableJson $item) { + // + }); + } + + public function testFailsWhenNotInteractingWithAllPropsInScope() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found in scope [bar].'); + + $assert->has('bar', function (AssertableJson $item) { + $item->where('baz', 'example'); + }); + } + + public function testDisableInteractionCheckForCurrentScope() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $assert->has('bar', function (AssertableJson $item) { + $item->etc(); + }); + } + + public function testCannotDisableInteractionCheckForDifferentScopes() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => [ + 'foo' => 'bar', + 'example' => 'value', + ], + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found in scope [bar.baz].'); + + $assert->has('bar', function (AssertableJson $item) { + $item + ->etc() + ->has('baz', function (AssertableJson $item) { + // + }); + }); + } + + public function testTopLevelPropInteractionDisabledByDefault() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + 'bar' => 'baz', + ]); + + $assert->has('foo'); + } + + public function testTopLevelInteractionEnabledWhenInteractedFlagSet() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + 'bar' => 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found on the root level.'); + + $assert + ->has('foo') + ->interacted(); + } + + public function testAssertWhereAllMatchesValues() + { + $assert = AssertableJson::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->whereAll([ + 'foo.bar' => 'value', + 'foo.example' => ArrayableStubObject::make(['hello' => 'world']), + 'baz' => function ($value) { + return $value === 'another'; + }, + ]); + } + + public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + 'baz' => 'example', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was marked as invalid using a closure.'); + + $assert->whereAll([ + 'foo' => 'bar', + 'baz' => function ($value) { + return $value === 'foo'; + }, + ]); + } + + public function testAssertWhereTypeString() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + ]); + + $assert->whereType('foo', 'string'); + } + + public function testAssertWhereTypeInteger() + { + $assert = AssertableJson::fromArray([ + 'foo' => 123, + ]); + + $assert->whereType('foo', 'integer'); + } + + public function testAssertWhereTypeBoolean() + { + $assert = AssertableJson::fromArray([ + 'foo' => true, + ]); + + $assert->whereType('foo', 'boolean'); + } + + public function testAssertWhereTypeDouble() + { + $assert = AssertableJson::fromArray([ + 'foo' => 12.3, + ]); + + $assert->whereType('foo', 'double'); + } + + public function testAssertWhereTypeArray() + { + $assert = AssertableJson::fromArray([ + 'foo' => ['bar', 'baz'], + 'bar' => ['foo' => 'baz'], + ]); + + $assert->whereType('foo', 'array'); + $assert->whereType('bar', 'array'); + } + + public function testAssertWhereTypeNull() + { + $assert = AssertableJson::fromArray([ + 'foo' => null, + ]); + + $assert->whereType('foo', 'null'); + } + + public function testAssertWhereAllType() + { + $assert = AssertableJson::fromArray([ + 'one' => 'foo', + 'two' => 123, + 'three' => true, + 'four' => 12.3, + 'five' => ['foo', 'bar'], + 'six' => ['foo' => 'bar'], + 'seven' => null, + ]); + + $assert->whereAllType([ + 'one' => 'string', + 'two' => 'integer', + 'three' => 'boolean', + 'four' => 'double', + 'five' => 'array', + 'six' => 'array', + 'seven' => 'null', + ]); + } + + public function testAssertWhereTypeWhenWrongTypeIsGiven() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo] is not of expected type [integer].'); + + $assert->whereType('foo', 'integer'); + } + + public function testAssertWhereTypeWithUnionTypes() + { + $firstAssert = AssertableJson::fromArray([ + 'foo' => 'bar', + ]); + + $secondAssert = AssertableJson::fromArray([ + 'foo' => null, + ]); + + $firstAssert->whereType('foo', ['string', 'null']); + $secondAssert->whereType('foo', ['string', 'null']); + } + + public function testAssertWhereTypeWhenWrongUnionTypeIsGiven() + { + $assert = AssertableJson::fromArray([ + 'foo' => 123, + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo] is not of expected type [string|null].'); + + $assert->whereType('foo', ['string', 'null']); + } + + public function testAssertWhereTypeWithPipeInUnionType() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + ]); + + $assert->whereType('foo', 'string|null'); + } + + public function testAssertWhereTypeWithPipeInWrongUnionType() + { + $assert = AssertableJson::fromArray([ + 'foo' => 'bar', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo] is not of expected type [integer|null].'); + + $assert->whereType('foo', 'integer|null'); + } + + public function testAssertHasAll() + { + $assert = AssertableJson::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->hasAll([ + 'foo.bar', + 'foo.example', + 'baz', + ]); + } + + public function testAssertHasAllFailsWhenAtLeastOnePropMissing() + { + $assert = AssertableJson::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.baz] does not exist.'); + + $assert->hasAll([ + 'foo.bar', + 'foo.baz', + 'baz', + ]); + } + + public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() + { + $assert = AssertableJson::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->hasAll('foo.bar', 'foo.example', 'baz'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.baz] does not exist.'); + + $assert->hasAll('foo.bar', 'foo.baz', 'baz'); + } + + public function testAssertCountMultipleProps() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'key' => 'value', + 'prop' => 'example', + ], + 'baz' => [ + 'another' => 'value', + ], + ]); + + $assert->hasAll([ + 'bar' => 2, + 'baz' => 1, + ]); + } + + public function testAssertCountMultiplePropsFailsWhenPropMissing() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'key' => 'value', + 'prop' => 'example', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->hasAll([ + 'bar' => 2, + 'baz' => 1, + ]); + } + + public function testMacroable() + { + AssertableJson::macro('myCustomMacro', function () { + throw new RuntimeException('My Custom Macro was called!'); + }); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('My Custom Macro was called!'); + + $assert = AssertableJson::fromArray(['foo' => 'bar']); + $assert->myCustomMacro(); + } + + public function testTappable() + { + $assert = AssertableJson::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $called = false; + $assert->has('bar', function (AssertableJson $assert) use (&$called) { + $assert->etc(); + $assert->tap(function (AssertableJson $assert) use (&$called) { + $called = true; + }); + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } +} diff --git a/tests/Testing/ParallelConsoleOutputTest.php b/tests/Testing/ParallelConsoleOutputTest.php new file mode 100644 index 000000000000..7e9da0244df1 --- /dev/null +++ b/tests/Testing/ParallelConsoleOutputTest.php @@ -0,0 +1,25 @@ +write('Running phpunit in 12 processes with laravel/laravel.'); + $this->assertEmpty($original->fetch()); + + $output->write('Configuration read from phpunit.xml.dist'); + $this->assertEmpty($original->fetch()); + + $output->write('... 3/3 (100%)'); + $this->assertSame('... 3/3 (100%)', $original->fetch()); + } +} diff --git a/tests/Testing/ParallelTestingTest.php b/tests/Testing/ParallelTestingTest.php new file mode 100644 index 000000000000..cb6fafb95382 --- /dev/null +++ b/tests/Testing/ParallelTestingTest.php @@ -0,0 +1,100 @@ +{$caller}($this); + $this->assertFalse($state); + + $parallelTesting->{$callback}(function ($token, $testCase = null) use ($callback, &$state) { + if (in_array($callback, ['setUpTestCase', 'tearDownTestCase'])) { + $this->assertSame($this, $testCase); + } else { + $this->assertNull($testCase); + } + + $this->assertEquals(1, $token); + $state = true; + }); + + $parallelTesting->{$caller}($this); + $this->assertFalse($state); + + $parallelTesting->resolveTokenUsing(function () { + return 1; + }); + + $parallelTesting->{$caller}($this); + $this->assertTrue($state); + } + + public function testOptions() + { + $parallelTesting = new ParallelTesting(Container::getInstance()); + + $this->assertFalse($parallelTesting->option('recreate_databases')); + + $parallelTesting->resolveOptionsUsing(function ($option) { + return $option === 'recreate_databases'; + }); + + $this->assertFalse($parallelTesting->option('recreate_caches')); + $this->assertTrue($parallelTesting->option('recreate_databases')); + } + + public function testToken() + { + $parallelTesting = new ParallelTesting(Container::getInstance()); + + $this->assertFalse($parallelTesting->token()); + + $parallelTesting->resolveTokenUsing(function () { + return 1; + }); + + $this->assertSame(1, $parallelTesting->token()); + } + + public function callbacks() + { + return [ + ['setUpProcess'], + ['setUpTestCase'], + ['setUpTestDatabase'], + ['tearDownTestCase'], + ['tearDownProcess'], + ]; + } + + public function tearDown(): void + { + parent::tearDown(); + + Container::setInstance(null); + + unset($_SERVER['LARAVEL_PARALLEL_TESTING']); + } +} diff --git a/tests/Testing/Stubs/ArrayableStubObject.php b/tests/Testing/Stubs/ArrayableStubObject.php new file mode 100644 index 000000000000..021440e0b287 --- /dev/null +++ b/tests/Testing/Stubs/ArrayableStubObject.php @@ -0,0 +1,25 @@ +data = $data; + } + + public static function make($data = []) + { + return new self($data); + } + + public function toArray() + { + return $this->data; + } +} diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index a231783eb804..7257b0159e58 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1,6 +1,6 @@ assertJson($resource->jsonSerialize()); } + public function testAssertJsonWithFluent() + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); + + $response->assertJson(function (AssertableJson $json) { + $json->where('0.foo', 'foo 0'); + }); + } + + public function testAssertJsonWithFluentFailsWhenNotInteractingWithAllProps() + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found on the root level.'); + + $response->assertJson(function (AssertableJson $json) { + $json->where('foo', 'bar'); + }); + } + + public function testAssertJsonWithFluentSkipsInteractionWhenTopLevelKeysNonAssociative() + { + $response = TestResponse::fromBaseResponse(new Response([ + ['foo' => 'bar'], + ['foo' => 'baz'], + ])); + + $response->assertJson(function (AssertableJson $json) { + // + }); + } + public function testAssertSimilarJsonWithMixed() { $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); @@ -1048,6 +1082,27 @@ public function testAssertJsonMissingValidationErrorsCanFail2() $response->assertJsonMissingValidationErrors('bar'); } + public function testAssertJsonMissingValidationErrorsCanFail3() + { + $this->expectException(AssertionFailedError::class); + + $baseResponse = tap(new Response, function ($response) { + $response->setContent( + json_encode([ + 'data' => [ + 'errors' => [ + 'foo' => ['one'], + ], + ], + ]), + ); + }); + + $response = TestResponse::fromBaseResponse($baseResponse); + + $response->assertJsonMissingValidationErrors('foo', 'data.errors'); + } + public function testAssertJsonMissingValidationErrorsWithoutArgument() { $data = ['status' => 'ok']; @@ -1109,6 +1164,31 @@ public function testAssertJsonMissingValidationErrorsCustomErrorsName() $testResponse->assertJsonMissingValidationErrors('bar', 'data'); } + public function testAssertJsonMissingValidationErrorsNestedCustomErrorsName1() + { + $data = [ + 'status' => 'ok', + 'data' => [ + 'errors' => ['foo' => 'oops'], + ], + ]; + + $testResponse = TestResponse::fromBaseResponse( + (new Response)->setContent(json_encode($data)) + ); + + $testResponse->assertJsonMissingValidationErrors('bar', 'data.errors'); + } + + public function testAssertJsonMissingValidationErrorsNestedCustomErrorsName2() + { + $testResponse = TestResponse::fromBaseResponse( + (new Response)->setContent(json_encode([])) + ); + + $testResponse->assertJsonMissingValidationErrors('bar', 'data.errors'); + } + public function testMacroable() { TestResponse::macro('foo', function () { @@ -1161,7 +1241,7 @@ public function testItCanBeTapped() public function testAssertPlainCookie() { $response = TestResponse::fromBaseResponse( - (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value')) + (new Response)->withCookie(new Cookie('cookie-name', 'cookie-value')) ); $response->assertPlainCookie('cookie-name', 'cookie-value'); @@ -1180,7 +1260,7 @@ public function testAssertCookie() $encryptedValue = $encrypter->encrypt(CookieValuePrefix::create($cookieName, $encrypter->getKey()).$cookieValue, false); $response = TestResponse::fromBaseResponse( - (new Response())->withCookie(new Cookie($cookieName, $encryptedValue)) + (new Response)->withCookie(new Cookie($cookieName, $encryptedValue)) ); $response->assertCookie($cookieName, $cookieValue); @@ -1189,7 +1269,7 @@ public function testAssertCookie() public function testAssertCookieExpired() { $response = TestResponse::fromBaseResponse( - (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', time() - 5000)) + (new Response)->withCookie(new Cookie('cookie-name', 'cookie-value', time() - 5000)) ); $response->assertCookieExpired('cookie-name'); @@ -1198,7 +1278,7 @@ public function testAssertCookieExpired() public function testAssertSessionCookieExpiredDoesNotTriggerOnSessionCookies() { $response = TestResponse::fromBaseResponse( - (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', 0)) + (new Response)->withCookie(new Cookie('cookie-name', 'cookie-value', 0)) ); $this->expectException(ExpectationFailedException::class); @@ -1209,7 +1289,7 @@ public function testAssertSessionCookieExpiredDoesNotTriggerOnSessionCookies() public function testAssertCookieNotExpired() { $response = TestResponse::fromBaseResponse( - (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', time() + 5000)) + (new Response)->withCookie(new Cookie('cookie-name', 'cookie-value', time() + 5000)) ); $response->assertCookieNotExpired('cookie-name'); @@ -1218,7 +1298,7 @@ public function testAssertCookieNotExpired() public function testAssertSessionCookieNotExpired() { $response = TestResponse::fromBaseResponse( - (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', 0)) + (new Response)->withCookie(new Cookie('cookie-name', 'cookie-value', 0)) ); $response->assertCookieNotExpired('cookie-name'); @@ -1226,7 +1306,7 @@ public function testAssertSessionCookieNotExpired() public function testAssertCookieMissing() { - $response = TestResponse::fromBaseResponse(new Response()); + $response = TestResponse::fromBaseResponse(new Response); $response->assertCookieMissing('cookie-name'); } diff --git a/tests/Validation/ValidationAddFailureTest.php b/tests/Validation/ValidationAddFailureTest.php index ec94133a18ed..20fa5e43706f 100644 --- a/tests/Validation/ValidationAddFailureTest.php +++ b/tests/Validation/ValidationAddFailureTest.php @@ -25,7 +25,7 @@ public function testAddFailureExists() $validator = $this->makeValidator(); $method_name = 'addFailure'; $this->assertTrue(method_exists($validator, $method_name)); - $this->assertTrue(is_callable([$validator, $method_name])); + $this->assertIsCallable([$validator, $method_name]); } public function testAddFailureIsFunctional() diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 3a97c0c962d1..d1d3d424dd59 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1096,10 +1096,34 @@ public function testRequiredIf() $v = new Validator($trans, ['foo' => true], ['bar' => 'required_if:foo,false']); $this->assertTrue($v->passes()); + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true], ['bar' => 'required_if:foo,null']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 0], ['bar' => 'required_if:foo,0']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '0'], ['bar' => 'required_if:foo,0']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 1], ['bar' => 'required_if:foo,1']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '1'], ['bar' => 'required_if:foo,1']); + $this->assertTrue($v->fails()); + $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['foo' => true], ['bar' => 'required_if:foo,true']); $this->assertTrue($v->fails()); + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false], ['bar' => 'required_if:foo,false']); + $this->assertTrue($v->fails()); + // error message when passed multiple values (required_if:foo,bar,baz) $trans = $this->getIlluminateArrayTranslator(); $trans->addLines(['validation.required_if' => 'The :attribute field is required when :other is :value.'], 'en'); @@ -1138,6 +1162,26 @@ public function testRequiredUnless() $v = new Validator($trans, ['foo' => false], ['bar' => 'required_unless:foo,true']); $this->assertTrue($v->fails()); + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true], ['bar' => 'required_unless:foo,null']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '0'], ['bar' => 'required_unless:foo,0']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 0], ['bar' => 'required_unless:foo,0']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '1'], ['bar' => 'required_unless:foo,1']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 1], ['bar' => 'required_unless:foo,1']); + $this->assertTrue($v->passes()); + // error message when passed multiple values (required_unless:foo,bar,baz) $trans = $this->getIlluminateArrayTranslator(); $trans->addLines(['validation.required_unless' => 'The :attribute field is required unless :other is in :values.'], 'en'); @@ -1146,6 +1190,108 @@ public function testRequiredUnless() $this->assertSame('The last field is required unless first is in taylor, sven.', $v->messages()->first('last')); } + public function testProhibited() + { + $trans = $this->getIlluminateArrayTranslator(); + + $v = new Validator($trans, [], ['name' => 'prohibited']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['last' => 'bar'], ['name' => 'prohibited']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['name' => 'foo'], ['name' => 'prohibited']); + $this->assertTrue($v->fails()); + + $file = new File('', false); + $v = new Validator($trans, ['name' => $file], ['name' => 'prohibited']); + $this->assertTrue($v->fails()); + + $file = new File(__FILE__, false); + $v = new Validator($trans, ['name' => $file], ['name' => 'prohibited']); + $this->assertTrue($v->fails()); + + $file = new File(__FILE__, false); + $file2 = new File(__FILE__, false); + $v = new Validator($trans, ['files' => [$file, $file2]], ['files.0' => 'prohibited', 'files.1' => 'prohibited']); + $this->assertTrue($v->fails()); + + $v = new Validator($trans, ['files' => [$file, $file2]], ['files' => 'prohibited']); + $this->assertTrue($v->fails()); + } + + public function testProhibitedIf() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_if:first,taylor']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor'], ['last' => 'prohibited_if:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true, 'bar' => 'baz'], ['bar' => 'prohibited_if:foo,false']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true, 'bar' => 'baz'], ['bar' => 'prohibited_if:foo,true']); + $this->assertTrue($v->fails()); + + // error message when passed multiple values (prohibited_if:foo,bar,baz) + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.prohibited_if' => 'The :attribute field is prohibited when :other is :value.'], 'en'); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertFalse($v->passes()); + $this->assertSame('The last field is prohibited when first is jess.', $v->messages()->first('last')); + } + + public function testProhibitedUnless() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_unless:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_unless:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false, 'bar' => 'baz'], ['bar' => 'prohibited_unless:foo,false']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false, 'bar' => 'baz'], ['bar' => 'prohibited_unless:foo,true']); + $this->assertTrue($v->fails()); + + // error message when passed multiple values (prohibited_unless:foo,bar,baz) + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.'], 'en'); + $v = new Validator($trans, ['first' => 'tim', 'last' => 'macdonald'], ['last' => 'prohibitedUnless:first,taylor,jess']); + $this->assertFalse($v->passes()); + $this->assertSame('The last field is prohibited unless first is in taylor, jess.', $v->messages()->first('last')); + } + public function testFailedFileUploads() { $trans = $this->getIlluminateArrayTranslator(); @@ -2249,6 +2395,15 @@ public function testValidateDistinct() $v->messages()->setFormat(':message'); $this->assertSame('There is a duplication!', $v->messages()->first('foo.0')); $this->assertSame('There is a duplication!', $v->messages()->first('foo.1')); + + $v = new Validator($trans, ['foo' => ['0100', '100']], ['foo.*' => 'distinct'], ['foo.*.distinct' => 'There is a duplication!']); + $this->assertFalse($v->passes()); + $v->messages()->setFormat(':message'); + $this->assertSame('There is a duplication!', $v->messages()->first('foo.0')); + $this->assertSame('There is a duplication!', $v->messages()->first('foo.1')); + + $v = new Validator($trans, ['foo' => ['0100', '100']], ['foo.*' => 'distinct:strict']); + $this->assertTrue($v->passes()); } public function testValidateDistinctForTopLevelArrays() @@ -2518,7 +2673,7 @@ public function testValidateEmailWithFilterUnicodeCheck() public function testValidateEmailWithCustomClassCheck() { $container = m::mock(Container::class); - $container->shouldReceive('make')->with(NoRFCWarningsValidation::class)->andReturn(new NoRFCWarningsValidation()); + $container->shouldReceive('make')->with(NoRFCWarningsValidation::class)->andReturn(new NoRFCWarningsValidation); $v = new Validator($this->getIlluminateArrayTranslator(), ['x' => 'foo@bar '], ['x' => 'email:'.NoRFCWarningsValidation::class]); $v->setContainer($container); @@ -2982,7 +3137,7 @@ public function testValidateImageDimensions() $v = new Validator($trans, ['x' => $uploadedFile], ['x' => 'dimensions:ratio=2/3']); $this->assertTrue($v->passes()); - // Ensure svg images always pass as size is irreleveant (image/svg+xml) + // Ensure svg images always pass as size is irrelevant (image/svg+xml) $svgXmlUploadedFile = new UploadedFile(__DIR__.'/fixtures/image.svg', '', 'image/svg+xml', null, true); $trans = $this->getIlluminateArrayTranslator(); @@ -2995,7 +3150,7 @@ public function testValidateImageDimensions() $v = new Validator($trans, ['x' => $svgXmlFile], ['x' => 'dimensions:max_width=1,max_height=1']); $this->assertTrue($v->passes()); - // Ensure svg images always pass as size is irreleveant (image/svg) + // Ensure svg images always pass as size is irrelevant (image/svg) $svgUploadedFile = new UploadedFile(__DIR__.'/fixtures/image2.svg', '', 'image/svg', null, true); $trans = $this->getIlluminateArrayTranslator(); @@ -3714,7 +3869,7 @@ public function testSometimesAddingRules() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => 'foo'], ['x' => 'Required']); $v->sometimes('x', 'Confirmed', function ($i) { - return $i->x == 'foo'; + return $i->x === 'foo'; }); $this->assertEquals(['x' => ['Required', 'Confirmed']], $v->getRules()); @@ -3728,21 +3883,21 @@ public function testSometimesAddingRules() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => 'foo'], ['x' => 'Required']); $v->sometimes('x', 'Confirmed', function ($i) { - return $i->x == 'bar'; + return $i->x === 'bar'; }); $this->assertEquals(['x' => ['Required']], $v->getRules()); $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => 'foo'], ['x' => 'Required']); $v->sometimes('x', 'Foo|Bar', function ($i) { - return $i->x == 'foo'; + return $i->x === 'foo'; }); $this->assertEquals(['x' => ['Required', 'Foo', 'Bar']], $v->getRules()); $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['x' => 'foo'], ['x' => 'Required']); $v->sometimes('x', ['Foo', 'Bar:Baz'], function ($i) { - return $i->x == 'foo'; + return $i->x === 'foo'; }); $this->assertEquals(['x' => ['Required', 'Foo', 'Bar:Baz']], $v->getRules()); @@ -3847,7 +4002,7 @@ public function testCustomDependentValidators() ['*.name' => 'dependent_rule:*.age'] ); $v->addDependentExtension('dependent_rule', function ($name) use ($v) { - return Arr::get($v->getData(), $name) == 'Jamie'; + return Arr::get($v->getData(), $name) === 'Jamie'; }); $this->assertTrue($v->passes()); } @@ -5122,7 +5277,7 @@ public function message() ); $this->assertFalse($v->passes()); - $this->assertTrue(is_array($v->failed()['foo.foo.bar'])); + $this->assertIsArray($v->failed()['foo.foo.bar']); } public function testImplicitCustomValidationObjects() @@ -5334,6 +5489,20 @@ public function providesPassingExcludeIfData() 'has_appointment' => false, ], ], + [ + [ + 'has_appointment' => ['nullable', 'bool'], + 'appointment_date' => ['exclude_if:has_appointment,null', 'required', 'date'], + ], + [ + 'has_appointment' => true, + 'appointment_date' => '2021-03-08', + ], + [ + 'has_appointment' => true, + 'appointment_date' => '2021-03-08', + ], + ], [ [ 'has_appointment' => ['required', 'bool'], @@ -5668,6 +5837,14 @@ public function testExcludeUnless() ); $this->assertTrue($validator->fails()); $this->assertSame(['mouse' => ['validation.required']], $validator->messages()->toArray()); + + $validator = new Validator( + $this->getIlluminateArrayTranslator(), + ['foo' => true, 'bar' => 'baz'], + ['foo' => 'nullable', 'bar' => 'exclude_unless:foo,null'] + ); + $this->assertTrue($validator->passes()); + $this->assertSame(['foo' => true], $validator->validated()); } public function testExcludeWithout() @@ -5719,6 +5896,42 @@ public function testValidateFailsWithAsterisksAsDataKeys() $this->assertSame(['data.1.date' => ['validation.date'], 'data.*.date' => ['validation.date']], $validator->messages()->toArray()); } + public function testFailOnFirstError() + { + $trans = $this->getIlluminateArrayTranslator(); + $data = [ + 'foo' => 'bar', + 'age' => 30, + ]; + $rules = [ + 'foo' => ['required', 'string'], + 'baz' => ['required'], + 'age' => ['required', 'min:31'], + ]; + + $expectedFailOnFirstErrorDisableResult = [ + 'baz' => [ + 'validation.required', + ], + 'age' => [ + 'validation.min.string', + ], + ]; + $failOnFirstErrorDisable = new Validator($trans, $data, $rules); + $this->assertFalse($failOnFirstErrorDisable->passes()); + $this->assertEquals($expectedFailOnFirstErrorDisableResult, $failOnFirstErrorDisable->getMessageBag()->getMessages()); + + $expectedFailOnFirstErrorEnableResult = [ + 'baz' => [ + 'validation.required', + ], + ]; + $failOnFirstErrorEnable = new Validator($trans, $data, $rules, [], []); + $failOnFirstErrorEnable->stopOnFirstFailure(); + $this->assertFalse($failOnFirstErrorEnable->passes()); + $this->assertEquals($expectedFailOnFirstErrorEnableResult, $failOnFirstErrorEnable->getMessageBag()->getMessages()); + } + protected function getTranslator() { return m::mock(TranslatorContract::class); diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 6872d38ae4a1..a00744859d45 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -13,7 +13,7 @@ class BladeComponentTagCompilerTest extends AbstractBladeTestCase { - public function tearDown(): void + protected function tearDown(): void { Mockery::close(); } @@ -40,69 +40,77 @@ public function testBasicComponentParsing() $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('
'); - $this->assertSame("
@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) + $this->assertSame("
##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes(['type' => 'foo','limit' => '5','@click' => 'foo','wire:click' => 'changePlan(\''.e(\$plan).'\')','required' => true]); ?>\n". -"@endcomponentClass @component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) +"@endComponentClass##END-COMPONENT-CLASS####BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes([]); ?>\n". -'@endcomponentClass
', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##
', trim($result)); } public function testBasicComponentWithEmptyAttributesParsing() { $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('
'); - $this->assertSame("
@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) + $this->assertSame("
##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes(['type' => '','limit' => '','@click' => '','required' => true]); ?>\n". -'@endcomponentClass
', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##
', trim($result)); } public function testDataCamelCasing() { $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => '1']) -withAttributes([]); ?> @endcomponentClass", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => '1']) +withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testColonData() { $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => 1]) -withAttributes([]); ?> @endcomponentClass", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => 1]) +withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); + } + + public function testEscapedColonAttribute() + { + $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); + + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => 1]) +withAttributes([':title' => 'user.name']); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testColonAttributesIsEscapedIfStrings() { $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) -withAttributes(['src' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('foo')]); ?> @endcomponentClass", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) +withAttributes(['src' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('foo')]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testColonNestedComponentParsing() { $result = $this->compiler(['foo:alert' => TestAlertComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'foo:alert', []) -withAttributes([]); ?> @endcomponentClass", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'foo:alert', []) +withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testColonStartingNestedComponentParsing() { $result = $this->compiler(['foo:alert' => TestAlertComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'foo:alert', []) -withAttributes([]); ?> @endcomponentClass", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'foo:alert', []) +withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testSelfClosingComponentsCanBeCompiled() { $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('
'); - $this->assertSame("
@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) + $this->assertSame("
##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes([]); ?>\n". -'@endcomponentClass
', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##
', trim($result)); } public function testClassNamesCanBeGuessed() @@ -139,18 +147,18 @@ public function testComponentsCanBeCompiledWithHyphenAttributes() $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes(['class' => 'bar','wire:model' => 'foo','x-on:click' => 'bar','@click' => 'baz']); ?>\n". -'@endcomponentClass', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##', trim($result)); } public function testSelfClosingComponentsCanBeCompiledWithDataAndAttributes() { $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => 'foo']) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => 'foo']) withAttributes(['class' => 'bar','wire:model' => 'foo']); ?>\n". -'@endcomponentClass', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##', trim($result)); } public function testComponentCanReceiveAttributeBag() @@ -158,8 +166,8 @@ public function testComponentCanReceiveAttributeBag() $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) -withAttributes(['class' => 'bar','attributes' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\$attributes),'wire:model' => 'foo']); ?> @endcomponentClass", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) +withAttributes(['class' => 'bar','attributes' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\$attributes),'wire:model' => 'foo']); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testSelfClosingComponentCanReceiveAttributeBag() @@ -168,35 +176,35 @@ public function testSelfClosingComponentCanReceiveAttributeBag() $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('
merge([\'class\' => \'test\']) }} wire:model="foo" />
'); - $this->assertSame("
@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => 'foo']) + $this->assertSame("
##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => 'foo']) withAttributes(['class' => 'bar','attributes' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\$attributes->merge(['class' => 'test'])),'wire:model' => 'foo']); ?>\n". - '@endcomponentClass
', trim($result)); + '@endComponentClass##END-COMPONENT-CLASS##
', trim($result)); } public function testComponentsCanHaveAttachedWord() { $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('Words'); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) -withAttributes([]); ?> @endcomponentClass Words", trim($result)); + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) +withAttributes([]); ?> @endComponentClass##END-COMPONENT-CLASS##Words", trim($result)); } public function testSelfClosingComponentsCanHaveAttachedWord() { $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('Words'); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes([]); ?>\n". -'@endcomponentClass Words', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##Words', trim($result)); } public function testSelfClosingComponentsCanBeCompiledWithBoundData() { $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(''); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => \$title]) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => \$title]) withAttributes(['class' => 'bar']); ?>\n". -'@endcomponentClass', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##', trim($result)); } public function testPairedComponentTags() @@ -204,9 +212,9 @@ public function testPairedComponentTags() $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(' '); - $this->assertSame("@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) withAttributes([]); ?> - @endcomponentClass", trim($result)); + @endComponentClass##END-COMPONENT-CLASS##", trim($result)); } public function testClasslessComponents() @@ -220,9 +228,9 @@ public function testClasslessComponents() $result = $this->compiler()->compileTags(''); - $this->assertSame("@component('Illuminate\View\AnonymousComponent', 'anonymous-component', ['view' => 'components.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']]) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'anonymous-component', ['view' => 'components.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']]) withAttributes(['name' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('Taylor'),'age' => 31,'wire:model' => 'foo']); ?>\n". -'@endcomponentClass', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##', trim($result)); } public function testPackagesClasslessComponents() @@ -236,9 +244,9 @@ public function testPackagesClasslessComponents() $result = $this->compiler()->compileTags(''); - $this->assertSame("@component('Illuminate\View\AnonymousComponent', 'package::anonymous-component', ['view' => 'package::components.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']]) + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'package::anonymous-component', ['view' => 'package::components.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']]) withAttributes(['name' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('Taylor'),'age' => 31,'wire:model' => 'foo']); ?>\n". -'@endcomponentClass', trim($result)); +'@endComponentClass##END-COMPONENT-CLASS##', trim($result)); } public function testAttributeSanitization() diff --git a/tests/View/Blade/BladeInjectTest.php b/tests/View/Blade/BladeInjectTest.php new file mode 100644 index 000000000000..07ffd19f0e3f --- /dev/null +++ b/tests/View/Blade/BladeInjectTest.php @@ -0,0 +1,34 @@ + bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testDependenciesInjectedAsStringsAreCompiledWhenInjectedWithDoubleQuotes() + { + $string = 'Foo @inject("baz", "SomeNamespace\SomeClass") bar'; + $expected = 'Foo bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testDependenciesAreCompiled() + { + $string = "Foo @inject('baz', SomeNamespace\SomeClass::class) bar"; + $expected = "Foo bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testDependenciesAreCompiledWithDoubleQuotes() + { + $string = 'Foo @inject("baz", SomeNamespace\SomeClass::class) bar'; + $expected = "Foo bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } +} diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index 124d954d454e..6185daa23ddc 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -19,7 +19,7 @@ class ComponentTest extends TestCase protected $viewFactory; protected $config; - public function setUp(): void + protected function setUp(): void { $this->config = m::mock(Config::class); @@ -52,7 +52,7 @@ public function testInlineViewsGetCreated() $this->viewFactory->shouldReceive('exists')->once()->andReturn(false); $this->viewFactory->shouldReceive('addNamespace')->once()->with('__components', '/tmp'); - $component = new TestInlineViewComponent(); + $component = new TestInlineViewComponent; $this->assertSame('__components::c6327913fef3fca4518bcd7df1d0ff630758e241', $component->resolveView()); } @@ -61,7 +61,7 @@ public function testRegularViewsGetReturned() $view = m::mock(View::class); $this->viewFactory->shouldReceive('make')->once()->with('alert', [], [])->andReturn($view); - $component = new TestRegularViewComponent(); + $component = new TestRegularViewComponent; $this->assertSame($view, $component->resolveView()); } @@ -71,14 +71,14 @@ public function testRegularViewNamesGetReturned() $this->viewFactory->shouldReceive('exists')->once()->andReturn(true); $this->viewFactory->shouldReceive('addNamespace')->never(); - $component = new TestRegularViewNameViewComponent(); + $component = new TestRegularViewNameViewComponent; $this->assertSame('alert', $component->resolveView()); } public function testHtmlablesGetReturned() { - $component = new TestHtmlableReturningViewComponent(); + $component = new TestHtmlableReturningViewComponent; $view = $component->resolveView(); diff --git a/tests/View/ViewBladeCompilerTest.php b/tests/View/ViewBladeCompilerTest.php index d74405747c02..fc959e184bcf 100644 --- a/tests/View/ViewBladeCompilerTest.php +++ b/tests/View/ViewBladeCompilerTest.php @@ -49,6 +49,17 @@ public function testCompileCompilesFileAndReturnsContents() { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); $files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(true); + $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); + $compiler->compile('foo'); + } + + public function testCompileCompilesFileAndReturnsContentsCreatingDirectory() + { + $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); + $files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(false); + $files->shouldReceive('makeDirectory')->once()->with(__DIR__, 0777, true, true); $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); $compiler->compile('foo'); } @@ -57,6 +68,7 @@ public function testCompileCompilesAndGetThePath() { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); $files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(true); $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); $compiler->compile('foo'); $this->assertSame('foo', $compiler->getPath()); @@ -73,6 +85,7 @@ public function testCompileWithPathSetBefore() { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); $files->shouldReceive('get')->once()->with('foo')->andReturn('Hello World'); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(true); $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', 'Hello World'); // set path before compilation $compiler->setPath('foo'); @@ -103,6 +116,7 @@ public function testIncludePathToTemplate($content, $compiled) { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); $files->shouldReceive('get')->once()->with('foo')->andReturn($content); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(true); $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('foo').'.php', $compiled); $compiler->compile('foo'); @@ -157,6 +171,7 @@ public function testDontIncludeEmptyPath() { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); $files->shouldReceive('get')->once()->with('')->andReturn('Hello World'); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(true); $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1('').'.php', 'Hello World'); $compiler->setPath(''); $compiler->compile(); @@ -166,6 +181,7 @@ public function testDontIncludeNullPath() { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); $files->shouldReceive('get')->once()->with(null)->andReturn('Hello World'); + $files->shouldReceive('exists')->once()->with(__DIR__)->andReturn(true); $files->shouldReceive('put')->once()->with(__DIR__.'/'.sha1(null).'.php', 'Hello World'); $compiler->setPath(null); $compiler->compile(); diff --git a/tests/View/ViewComponentAttributeBagTest.php b/tests/View/ViewComponentAttributeBagTest.php index c91e74589e24..4fab0a42d37e 100644 --- a/tests/View/ViewComponentAttributeBagTest.php +++ b/tests/View/ViewComponentAttributeBagTest.php @@ -27,6 +27,9 @@ public function testAttributeRetrieval() $this->assertSame('font-bold', $bag->get('class')); $this->assertSame('bar', $bag->get('foo', 'bar')); $this->assertSame('font-bold', $bag['class']); + $this->assertSame('class="mt-4 font-bold" name="test"', (string) $bag->class('mt-4')); + $this->assertSame('class="mt-4 font-bold" name="test"', (string) $bag->class(['mt-4'])); + $this->assertSame('class="mt-4 ml-2 font-bold" name="test"', (string) $bag->class(['mt-4', 'ml-2' => true, 'mr-2' => false])); $bag = new ComponentAttributeBag([]); diff --git a/tests/View/ViewComponentTest.php b/tests/View/ViewComponentTest.php index 8ce0b90349f1..c7ac6cda96ec 100644 --- a/tests/View/ViewComponentTest.php +++ b/tests/View/ViewComponentTest.php @@ -62,7 +62,7 @@ public function testPublicMethodsWithNoArgsAreConvertedToStringableCallablesInvo public function testItIgnoresExceptedMethodsAndProperties() { - $component = new TestExceptedViewComponent(); + $component = new TestExceptedViewComponent; $variables = $component->data(); // Ignored methods (with no args) are not invoked behind the scenes. @@ -75,7 +75,7 @@ public function testItIgnoresExceptedMethodsAndProperties() public function testMethodsOverridePropertyValues() { - $component = new TestHelloPropertyHelloMethodComponent(); + $component = new TestHelloPropertyHelloMethodComponent; $variables = $component->data(); $this->assertArrayHasKey('hello', $variables); $this->assertSame('world', $variables['hello']()); diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 52061422074e..1419f502706c 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -7,6 +7,7 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Contracts\View\Engine; +use Illuminate\Contracts\View\View as ViewContract; use Illuminate\Events\Dispatcher; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\HtmlString; @@ -87,6 +88,7 @@ public function testFirstCreatesNewViewInstanceWithProperPath() $factory->addExtension('php', 'php'); $view = $factory->first(['bar', 'view'], ['foo' => 'bar'], ['baz' => 'boom']); + $this->assertInstanceOf(ViewContract::class, $view); $this->assertSame($engine, $view->getEngine()); $this->assertSame($_SERVER['__test.view'], $view); @@ -441,6 +443,27 @@ public function testMultipleStackPush() $this->assertSame('hi, Hello!', $factory->yieldPushContent('foo')); } + public function testSingleStackPrepend() + { + $factory = $this->getFactory(); + $factory->startPrepend('foo'); + echo 'hi'; + $factory->stopPrepend(); + $this->assertSame('hi', $factory->yieldPushContent('foo')); + } + + public function testMultipleStackPrepend() + { + $factory = $this->getFactory(); + $factory->startPrepend('foo'); + echo ', Hello!'; + $factory->stopPrepend(); + $factory->startPrepend('foo'); + echo 'hi'; + $factory->stopPrepend(); + $this->assertSame('hi, Hello!', $factory->yieldPushContent('foo')); + } + public function testSessionAppending() { $factory = $this->getFactory(); diff --git a/tests/View/ViewTest.php b/tests/View/ViewTest.php index d4a3047a38de..308e2e81e0f7 100755 --- a/tests/View/ViewTest.php +++ b/tests/View/ViewTest.php @@ -127,12 +127,12 @@ public function testViewAcceptsArrayableImplementations() public function testViewGettersSetters() { $view = $this->getView(['foo' => 'bar']); - $this->assertEquals('view', $view->name()); - $this->assertEquals('path', $view->getPath()); + $this->assertSame('view', $view->name()); + $this->assertSame('path', $view->getPath()); $data = $view->getData(); - $this->assertEquals('bar', $data['foo']); + $this->assertSame('bar', $data['foo']); $view->setPath('newPath'); - $this->assertEquals('newPath', $view->getPath()); + $this->assertSame('newPath', $view->getPath()); } public function testViewArrayAccess() @@ -140,9 +140,9 @@ public function testViewArrayAccess() $view = $this->getView(['foo' => 'bar']); $this->assertInstanceOf(ArrayAccess::class, $view); $this->assertTrue($view->offsetExists('foo')); - $this->assertEquals('bar', $view->offsetGet('foo')); + $this->assertSame('bar', $view->offsetGet('foo')); $view->offsetSet('foo', 'baz'); - $this->assertEquals('baz', $view->offsetGet('foo')); + $this->assertSame('baz', $view->offsetGet('foo')); $view->offsetUnset('foo'); $this->assertFalse($view->offsetExists('foo')); } @@ -152,9 +152,9 @@ public function testViewConstructedWithObjectData() $view = $this->getView(new DataObjectStub); $this->assertInstanceOf(ArrayAccess::class, $view); $this->assertTrue($view->offsetExists('foo')); - $this->assertEquals('bar', $view->offsetGet('foo')); + $this->assertSame('bar', $view->offsetGet('foo')); $view->offsetSet('foo', 'baz'); - $this->assertEquals('baz', $view->offsetGet('foo')); + $this->assertSame('baz', $view->offsetGet('foo')); $view->offsetUnset('foo'); $this->assertFalse($view->offsetExists('foo')); } @@ -163,9 +163,9 @@ public function testViewMagicMethods() { $view = $this->getView(['foo' => 'bar']); $this->assertTrue(isset($view->foo)); - $this->assertEquals('bar', $view->foo); + $this->assertSame('bar', $view->foo); $view->foo = 'baz'; - $this->assertEquals('baz', $view->foo); + $this->assertSame('baz', $view->foo); $this->assertEquals($view['foo'], $view->foo); unset($view->foo); $this->assertFalse(isset($view->foo)); @@ -208,8 +208,8 @@ public function testViewRenderSections() $view->getFactory()->shouldReceive('getSections')->once()->andReturn(['foo', 'bar']); $sections = $view->renderSections(); - $this->assertEquals('foo', $sections[0]); - $this->assertEquals('bar', $sections[1]); + $this->assertSame('foo', $sections[0]); + $this->assertSame('bar', $sections[1]); } public function testWithErrors() @@ -219,18 +219,18 @@ public function testWithErrors() $this->assertSame($view, $view->withErrors($errors)); $this->assertInstanceOf(ViewErrorBag::class, $view->errors); $foo = $view->errors->get('foo'); - $this->assertEquals('bar', $foo[0]); + $this->assertSame('bar', $foo[0]); $qu = $view->errors->get('qu'); - $this->assertEquals('ux', $qu[0]); + $this->assertSame('ux', $qu[0]); $data = ['foo' => 'baz']; $this->assertSame($view, $view->withErrors(new MessageBag($data))); $foo = $view->errors->get('foo'); - $this->assertEquals('baz', $foo[0]); + $this->assertSame('baz', $foo[0]); $foo = $view->errors->getBag('default')->get('foo'); - $this->assertEquals('baz', $foo[0]); + $this->assertSame('baz', $foo[0]); $this->assertSame($view, $view->withErrors(new MessageBag($data), 'login')); $foo = $view->errors->getBag('login')->get('foo'); - $this->assertEquals('baz', $foo[0]); + $this->assertSame('baz', $foo[0]); } protected function getView($data = []) diff --git a/tests/View/fixtures/nested/basic.php b/tests/View/fixtures/nested/basic.php index 557db03de997..e69de29bb2d1 100755 --- a/tests/View/fixtures/nested/basic.php +++ b/tests/View/fixtures/nested/basic.php @@ -1 +0,0 @@ -Hello World