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 42fe7239751e..ddaaa3fc63b2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,13 @@ on: jobs: linux_tests: - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 services: + memcached: + image: memcached:1.6-alpine + ports: + - 11211:11211 mysql: image: mysql:5.7 env: @@ -27,7 +31,7 @@ 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] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} @@ -40,16 +44,10 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached tools: composer:v2 coverage: none - - name: Setup Memcached - uses: niden/actions-memcached@v7 - - - name: Setup problem matchers - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Set Minimum Guzzle Version uses: nick-invision/retry@v1 with: @@ -77,7 +75,7 @@ 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] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - Windows @@ -95,13 +93,10 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, gd, pdo_mysql, fileinfo, ftp + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, gd, pdo_mysql, fileinfo, ftp, redis, memcached 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: diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index ae564658b926..5fd976698b85 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,86 @@ # Release Notes for 6.x -## [Unreleased](https://github.com/laravel/framework/compare/v6.20.5...6.x) +## [Unreleased](https://github.com/laravel/framework/compare/v6.20.17...6.x) + + +## [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) + +### Fixed +- Backport for fix issue with polymorphic morphMaps with literal 0 ([#35487](https://github.com/laravel/framework/pull/35487)) +- Fixed mime validation for jpeg files ([#35518](https://github.com/laravel/framework/pull/35518)) + + +## [v6.20.6 (2020-12-01)](https://github.com/laravel/framework/compare/v6.20.5...v6.20.6) + +### Fixed +- Backport Redis context option ([#35370](https://github.com/laravel/framework/pull/35370)) +- Fixed validating image/jpeg images after Symfony/Mime update ([#35419](https://github.com/laravel/framework/pull/35419)) ## [v6.20.5 (2020-11-24)](https://github.com/laravel/framework/compare/v6.20.4...v6.20.5) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index 55e5ebb05309..28603d11124f 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,9 +1,341 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.16.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.30.1...8.x) -## [v8.16.0 (2020-11-17)](https://github.com/laravel/framework/compare/v8.15.0...v8.16.0) +## [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) + +### Added +- Delay pushing jobs to queue until database transactions are committed ([#35422](https://github.com/laravel/framework/pull/35422), [095d922](https://github.com/laravel/framework/commit/095d9221e837e7a7d8bd00d14184e619b962173a), [fa34d93](https://github.com/laravel/framework/commit/fa34d93ad0cda78e8956b2680ba7bada02bcabb2), [db0d0ba](https://github.com/laravel/framework/commit/db0d0ba94cf8a5c1e1fa4621bd4a16032aff800d), [d9b803a](https://github.com/laravel/framework/commit/d9b803a1a4898e7d5b3145e51c77499815ce3401), [3e55841](https://github.com/laravel/framework/commit/3e5584165fb66d8228bae79a856eac51ce147df5)) +- Added `Illuminate\View\ComponentAttributeBag::has()` ([#35562](https://github.com/laravel/framework/pull/35562)) +- Create ScheduleListCommand ([#35574](https://github.com/laravel/framework/pull/35574), [97d7834](https://github.com/laravel/framework/commit/97d783449c5330b1e5fb9104f6073869ad3079c1)) +- Introducing Job Encryption ([#35527](https://github.com/laravel/framework/pull/35527), [f80f647](https://github.com/laravel/framework/commit/f80f647852106942e4a0ef3e9963f8f7a99122cf), [8c16156](https://github.com/laravel/framework/commit/8c16156636311e42883d9e84a6d71fa135bc2b73)) + +### Fixed +- Handle `Throwable` exceptions on `Illuminate\Redis\Limiters\ConcurrencyLimiter::block()` ([#35546](https://github.com/laravel/framework/pull/35546)) +- Fixed PDO passing in SqlServerDriver ([#35564](https://github.com/laravel/framework/pull/35564)) +- When following redirects, terminate each test request in proper order ([#35604](https://github.com/laravel/framework/pull/35604)) + + +## [v8.18.1 (2020-12-09)](https://github.com/laravel/framework/compare/v8.18.0...v8.18.1) + +### Fixed +- Bumped minimum Symfony version ([#35535](https://github.com/laravel/framework/pull/35535)) +- Fixed passing model instances to factories ([#35541](https://github.com/laravel/framework/pull/35541)) + + +## [v8.18.0 (2020-12-08)](https://github.com/laravel/framework/compare/v8.17.2...v8.18.0) + +### Added +- Added `Illuminate\Http\Client\Factory::assertSentInOrder()` ([#35525](https://github.com/laravel/framework/pull/35525), [d257ce2](https://github.com/laravel/framework/commit/d257ce2e93dfe52151be3d0386fcc4ea281ca8d5), [2fd1411](https://github.com/laravel/framework/commit/2fd141158eb5aead8aa2afff51bcd98250b6bbe6)) +- Added `Illuminate\Http\Client\Response::handlerStats()` ([#35520](https://github.com/laravel/framework/pull/35520)) +- Added support for attaching existing model instances in factories ([#35494](https://github.com/laravel/framework/pull/35494)) +- Added `assertChained()` and `assertDispatchedWithoutChain()` methods to `Illuminate\Support\Testing\Fakes\BusFake` class ([#35523](https://github.com/laravel/framework/pull/35523), [f1b8cac](https://github.com/laravel/framework/commit/f1b8cacfe2a8863894e258ce35a77decedbea36f), [236c67d](https://github.com/laravel/framework/commit/236c67db52f755bb475ba325148e9053733968aa)) +- Allow testing of html and plain text bodies right off mailables ([afb858a](https://github.com/laravel/framework/commit/afb858ad9c944bd3f9ad56c3e4485527d77a7327), [b7391e4](https://github.com/laravel/framework/commit/b7391e486fc68c1c422668a277eaac2bcbe72b2b)) + +### Fixed +- Fixed Application flush method ([#35482](https://github.com/laravel/framework/pull/35482)) +- Fixed mime validation for jpeg files ([#35518](https://github.com/laravel/framework/pull/35518)) + +### Revert +- Revert [Added ability to define table name as default morph type](https://github.com/laravel/framework/pull/35257) ([#35533](https://github.com/laravel/framework/pull/35533)) + + +## [v8.17.2 (2020-12-03)](https://github.com/laravel/framework/compare/v8.17.1...v8.17.2) + +### Added +- Added `Illuminate\Database\Eloquent\Relations\BelongsToMany::orderByPivot()` ([#35455](https://github.com/laravel/framework/pull/35455), [6f83a50](https://github.com/laravel/framework/commit/6f83a5099725dc47fbec1b0cf1bcc64f80f9dc86)) + + +## [v8.17.1 (2020-12-02)](https://github.com/laravel/framework/compare/v8.17.0...v8.17.1) + +### Fixed +- Fixed an issue with the database queue driver ([#35449](https://github.com/laravel/framework/pull/35449)) + + +## [v8.17.0 (2020-12-01)](https://github.com/laravel/framework/compare/v8.16.1...v8.17.0) + +### Added +- Added: Transaction aware code execution ([#35373](https://github.com/laravel/framework/pull/35373), [9565598](https://github.com/laravel/framework/commit/95655988ea1fb0c260ca792751e2e9da81afc3a7)) +- Added dd() and dump() to the request object ([#35384](https://github.com/laravel/framework/pull/35384), [c43e08f](https://github.com/laravel/framework/commit/c43e08f98afe5dcf742956510e9ab170ea11ce45)) +- Enqueue all jobs using a enqueueUsing method ([#35415](https://github.com/laravel/framework/pull/35415), [010d4d7](https://github.com/laravel/framework/commit/010d4d7ea7ec5581dfbf8b6ba84b812f8e4cb649), [#35437](https://github.com/laravel/framework/pull/35437)) + +### Fixed +- Fix issue with polymorphic morphMaps with literal 0 ([#35364](https://github.com/laravel/framework/pull/35364)) +- Fixed Self-Relation issue in withAggregate method ([#35392](https://github.com/laravel/framework/pull/35392), [aec5cca](https://github.com/laravel/framework/commit/aec5cca4ace65bc4b4ca054170b645f1073ac9ca), [#35394](https://github.com/laravel/framework/pull/35394)) +- Fixed Use PHP_EOL instead of `\n` in PendingCommand ([#35409](https://github.com/laravel/framework/pull/35409)) +- Fixed validating image/jpeg images after Symfony/Mime update ([#35419](https://github.com/laravel/framework/pull/35419)) +- Fixed fail to morph with custom casting to objects ([#35420](https://github.com/laravel/framework/pull/35420)) +- Fixed `Illuminate\Collections\Collection::sortBy()` ([307f6fb](https://github.com/laravel/framework/commit/307f6fb8d9579427a9521a07e8700355a3e9d948)) +- Don't overwrite minute and hour when specifying a time with twiceMonthly() ([#35436](https://github.com/laravel/framework/pull/35436)) + +### Changed +- Make DownCommand retryAfter available to prerendered view ([#35357](https://github.com/laravel/framework/pull/35357), [b1ee97e](https://github.com/laravel/framework/commit/b1ee97e5ae03dae293e3256b8c3013209d0fd9b0)) +- Set default value on cloud driver ([0bb7fe4](https://github.com/laravel/framework/commit/0bb7fe4758d617b07b84f6fabfcfe2ca2cdb0964)) +- Update Tailwind pagination focus styles ([#35365](https://github.com/laravel/framework/pull/35365)) +- Redis: allow to pass connection name ([#35402](https://github.com/laravel/framework/pull/35402)) +- Change Wormhole to use the Date Factory ([#35421](https://github.com/laravel/framework/pull/35421)) + + +## [v8.16.1 (2020-11-25)](https://github.com/laravel/framework/compare/v8.16.0...v8.16.1) + +### Fixed +- Fixed reflection exception in `Illuminate\Routing\Router::gatherRouteMiddleware()` ([c6e8357](https://github.com/laravel/framework/commit/c6e8357e19b10a800df8a67446f23310f4e83d1f)) + + +## [v8.16.0 (2020-11-24)](https://github.com/laravel/framework/compare/v8.15.0...v8.16.0) ### Added - Added `Illuminate\Console\Concerns\InteractsWithIO::withProgressBar()` ([4e52a60](https://github.com/laravel/framework/commit/4e52a606e91619f6082ed8d46f8d64f9d4dbd0b2), [169fd2b](https://github.com/laravel/framework/commit/169fd2b5156650a067aa77a38681875d2a6c5e57)) diff --git a/composer.json b/composer.json index a63c064c721a..401a6e952839 100644 --- a/composer.json +++ b/composer.json @@ -31,15 +31,15 @@ "psr/simple-cache": "^1.0", "ramsey/uuid": "^4.0", "swiftmailer/swiftmailer": "^6.0", - "symfony/console": "^5.1", - "symfony/error-handler": "^5.1", - "symfony/finder": "^5.1", - "symfony/http-foundation": "^5.1", - "symfony/http-kernel": "^5.1", - "symfony/mime": "^5.1", - "symfony/process": "^5.1", - "symfony/routing": "^5.1", - "symfony/var-dumper": "^5.1", + "symfony/console": "^5.1.4", + "symfony/error-handler": "^5.1.4", + "symfony/finder": "^5.1.4", + "symfony/http-foundation": "^5.1.4", + "symfony/http-kernel": "^5.1.4", + "symfony/mime": "^5.1.4", + "symfony/process": "^5.1.4", + "symfony/routing": "^5.1.4", + "symfony/var-dumper": "^5.1.4", "tijsverkoyen/css-to-inline-styles": "^2.2.2", "vlucas/phpdotenv": "^5.2", "voku/portable-ascii": "^1.4.8" @@ -84,11 +84,11 @@ "guzzlehttp/guzzle": "^6.5.5|^7.0.1", "league/flysystem-cached-adapter": "^1.0", "mockery/mockery": "^1.4.2", - "orchestra/testbench-core": "^6.5", + "orchestra/testbench-core": "^6.8", "pda/pheanstalk": "^4.0", "phpunit/phpunit": "^8.5.8|^9.3.3", "predis/predis": "^1.1.1", - "symfony/cache": "^5.1" + "symfony/cache": "^5.1.4" }, "provide": { "psr/container-implementation": "1.0" @@ -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,9 +144,9 @@ "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).", - "symfony/cache": "Required to PSR-6 cache bridge (^5.1).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^5.1).", + "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).", "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)." }, diff --git a/docker-compose.yml b/docker-compose.yml index dc02296a48b4..4b129f911cfc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: memcached: - image: memcached:1.5-alpine + image: memcached:1.6-alpine ports: - "11211:11211" restart: always diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3cb5a6c6ba63..bb20f5f6ded1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,14 +17,6 @@ ./tests - - - ./src - - - ./src/ - - - - + + 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/Notifications/VerifyEmail.php b/src/Illuminate/Auth/Notifications/VerifyEmail.php index 5863193a0840..7a5cf916449d 100644 --- a/src/Illuminate/Auth/Notifications/VerifyEmail.php +++ b/src/Illuminate/Auth/Notifications/VerifyEmail.php @@ -56,7 +56,7 @@ public function toMail($notifiable) /** * Get the verify email notification mail message for the given URL. * - * @param string $verificationUrl + * @param string $url * @return \Illuminate\Notifications\Messages\MailMessage */ protected function buildMailMessage($url) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index ac1629b4b528..1f6863141c4c 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -29,7 +29,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth use GuardHelpers, Macroable; /** - * The name of the Guard. Typically "session". + * The name of the guard. Typically "web". * * Corresponds to guard name in authentication configuration. * @@ -320,7 +320,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 diff --git a/src/Illuminate/Broadcasting/BroadcastEvent.php b/src/Illuminate/Broadcasting/BroadcastEvent.php index 775df78059d7..e9c0897a553b 100644 --- a/src/Illuminate/Broadcasting/BroadcastEvent.php +++ b/src/Illuminate/Broadcasting/BroadcastEvent.php @@ -46,6 +46,7 @@ public function __construct($event) $this->event = $event; $this->tries = property_exists($event, 'tries') ? $event->tries : null; $this->timeout = property_exists($event, 'timeout') ? $event->timeout : null; + $this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null; } /** 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 @@ +afterCommit = true; + + return $this; + } + + /** + * Indicate that the job should not wait until database transactions have been committed before dispatching. + * + * @return $this + */ + public function beforeCommit() + { + $this->afterCommit = false; + + return $this; + } + /** * Specify the middleware the job should be dispatched through. * diff --git a/src/Illuminate/Bus/UpdatedBatchJobCounts.php b/src/Illuminate/Bus/UpdatedBatchJobCounts.php index cc4bdd700b7c..83d33a44f2f7 100644 --- a/src/Illuminate/Bus/UpdatedBatchJobCounts.php +++ b/src/Illuminate/Bus/UpdatedBatchJobCounts.php @@ -32,7 +32,7 @@ public function __construct(int $pendingJobs = 0, int $failedJobs = 0) } /** - * Determine if all jobs have ran exactly once. + * Determine if all jobs have run exactly once. * * @return bool */ diff --git a/src/Illuminate/Cache/CacheLock.php b/src/Illuminate/Cache/CacheLock.php index f4a1407c1cbf..310d9fb5d35c 100644 --- a/src/Illuminate/Cache/CacheLock.php +++ b/src/Illuminate/Cache/CacheLock.php @@ -14,7 +14,7 @@ class CacheLock extends Lock /** * Create a new lock instance. * - * @var \Illuminate\Contracts\Cache\Store + * @param \Illuminate\Contracts\Cache\Store $store * @param string $name * @param int $seconds * @param string|null $owner diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 352ea6f50cc0..c8c8f602808b 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -199,7 +199,11 @@ protected function createRedisDriver(array $config) $connection = $config['connection'] ?? 'default'; - return $this->repository(new RedisStore($redis, $this->getPrefix($config), $connection)); + $store = new RedisStore($redis, $this->getPrefix($config), $connection); + + return $this->repository( + $store->setLockConnection($config['lock_connection'] ?? $connection) + ); } /** @@ -212,15 +216,17 @@ protected function createDatabaseDriver(array $config) { $connection = $this->app['db']->connection($config['connection'] ?? null); - return $this->repository( - new DatabaseStore( - $connection, - $config['table'], - $this->getPrefix($config), - $config['lock_table'] ?? 'cache_locks', - $config['lock_lottery'] ?? [2, 100] - ) + $store = new DatabaseStore( + $connection, + $config['table'], + $this->getPrefix($config), + $config['lock_table'] ?? 'cache_locks', + $config['lock_lottery'] ?? [2, 100] ); + + return $this->repository($store->setLockConnection( + $this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null) + )); } /** diff --git a/src/Illuminate/Cache/DatabaseLock.php b/src/Illuminate/Cache/DatabaseLock.php index 296f973bd2e2..7fd05c19134a 100644 --- a/src/Illuminate/Cache/DatabaseLock.php +++ b/src/Illuminate/Cache/DatabaseLock.php @@ -136,4 +136,14 @@ protected function getCurrentOwner() { return optional($this->connection->table($this->table)->where('key', $this->name)->first())->owner; } + + /** + * Get the name of the database connection being used to manage the lock. + * + * @return string + */ + public function getConnectionName() + { + return $this->connection->getName(); + } } diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index cce14063257c..32d7a9fc0676 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -23,6 +23,13 @@ class DatabaseStore implements LockProvider, Store */ protected $connection; + /** + * The database connection instance that should be used to manage locks. + * + * @var \Illuminate\Database\ConnectionInterface + */ + protected $lockConnection; + /** * The name of the cache table. * @@ -265,7 +272,7 @@ public function forever($key, $value) public function lock($name, $seconds = 0, $owner = null) { return new DatabaseLock( - $this->connection, + $this->lockConnection ?? $this->connection, $this->lockTable, $this->prefix.$name, $seconds, @@ -331,6 +338,19 @@ public function getConnection() return $this->connection; } + /** + * Specify the name of the connection that should be used to manage locks. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @return $this + */ + public function setLockConnection($connection) + { + $this->lockConnection = $connection; + + return $this; + } + /** * Get the cache key prefix. * 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/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/RedisLock.php b/src/Illuminate/Cache/RedisLock.php index 9f62eada9510..481b811d398f 100644 --- a/src/Illuminate/Cache/RedisLock.php +++ b/src/Illuminate/Cache/RedisLock.php @@ -70,4 +70,14 @@ protected function getCurrentOwner() { return $this->redis->get($this->name); } + + /** + * Get the name of the Redis connection being used to manage the lock. + * + * @return string + */ + public function getConnectionName() + { + return $this->redis->getName(); + } } diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index f3aa8a3dce34..cdf1c8fca094 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -22,12 +22,19 @@ class RedisStore extends TaggableStore implements LockProvider protected $prefix; /** - * The Redis connection that should be used. + * The Redis connection instance that should be used to manage locks. * * @var string */ protected $connection; + /** + * The name of the connection that should be used for locks. + * + * @var string + */ + protected $lockConnection; + /** * Create a new Redis store. * @@ -181,7 +188,7 @@ public function forever($key, $value) */ public function lock($name, $seconds = 0, $owner = null) { - return new RedisLock($this->connection(), $this->prefix.$name, $seconds, $owner); + return new RedisLock($this->lockConnection(), $this->prefix.$name, $seconds, $owner); } /** @@ -243,7 +250,17 @@ public function connection() } /** - * Set the connection name to be used. + * Get the Redis connection instance that should be used to manage locks. + * + * @return \Illuminate\Redis\Connections\Connection + */ + public function lockConnection() + { + return $this->redis->connection($this->lockConnection ?? $this->connection); + } + + /** + * Specify the name of the connection that should be used to store data. * * @param string $connection * @return void @@ -253,6 +270,19 @@ public function setConnection($connection) $this->connection = $connection; } + /** + * Specify the name of the connection that should be used to manage locks. + * + * @param string $connection + * @return $this + */ + public function setLockConnection($connection) + { + $this->lockConnection = $connection; + + return $this; + } + /** * Get the Redis database instance. * 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/Cache/composer.json b/src/Illuminate/Cache/composer.json index c802b1213aca..b9ef4881b062 100755 --- a/src/Illuminate/Cache/composer.json +++ b/src/Illuminate/Cache/composer.json @@ -35,7 +35,7 @@ "illuminate/database": "Required to use the database cache driver (^8.0).", "illuminate/filesystem": "Required to use the file cache driver (^8.0).", "illuminate/redis": "Required to use the redis cache driver (^8.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^5.1)." + "symfony/cache": "Required to PSR-6 cache bridge (^5.1.4)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index ae772b70cbac..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); } @@ -1128,7 +1126,7 @@ public function sortDesc($options = SORT_REGULAR) */ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) { - if (is_array($callback)) { + if (is_array($callback) && ! is_callable($callback)) { return $this->sortByMany($callback); } @@ -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..9b90792925b4 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -547,7 +547,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 +556,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 +837,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. * 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/composer.json b/src/Illuminate/Collections/composer.json index 268fae17d6f6..cb23d3e49486 100644 --- a/src/Illuminate/Collections/composer.json +++ b/src/Illuminate/Collections/composer.json @@ -32,7 +32,7 @@ } }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^5.1)." + "symfony/var-dumper": "Required to use the dump method (^5.1.4)." }, "config": { "sort-packages": true 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..a680c1a64cb0 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 */ @@ -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 */ diff --git a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php index 540217dd3c4b..7e1cdf17c0a8 100644 --- a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php +++ b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php @@ -425,9 +425,7 @@ public function twiceMonthly($first = 1, $second = 16, $time = '0:0') $this->dailyAt($time); - return $this->spliceIntoPosition(1, 0) - ->spliceIntoPosition(2, 0) - ->spliceIntoPosition(3, $daysOfMonth); + return $this->spliceIntoPosition(3, $daysOfMonth); } /** diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php new file mode 100644 index 000000000000..a46f9c7f1502 --- /dev/null +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -0,0 +1,52 @@ +events() as $event) { + $rows[] = [ + $event->command, + $event->expression, + $event->description, + (new CronExpression($event->expression)) + ->getNextRunDate(Carbon::now()) + ->setTimezone($this->option('timezone', config('app.timezone'))), + ]; + } + + $this->table([ + 'Command', + 'Interval', + 'Description', + 'Next Due', + ], $rows ?? []); + } +} diff --git a/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php b/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php new file mode 100644 index 000000000000..2d15888bbbf8 --- /dev/null +++ b/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php @@ -0,0 +1,47 @@ +events(); + + $commandNames = []; + + foreach ($commands as $command) { + $commandNames[] = $command->command; + } + + $index = array_search($this->choice('Which command would you like to run?', $commandNames), $commandNames); + + $event = $commands[$index]; + + $this->line('Running scheduled command: '.$event->getSummaryForDisplay()); + + $event->run($this->laravel); + } +} diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index dda1f3392aa9..46aaada73dd9 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -19,8 +19,8 @@ "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0", - "symfony/console": "^5.1", - "symfony/process": "^5.1" + "symfony/console": "^5.1.4", + "symfony/process": "^5.1.4" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 9403e6a0e546..2cfa72f51bb7 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -626,7 +626,7 @@ public function factory($abstract) /** * An alias function name for make(). * - * @param string $abstract + * @param string|callable $abstract * @param array $parameters * @return mixed * @@ -640,7 +640,7 @@ public function makeWith($abstract, array $parameters = []) /** * Resolve the given type from the container. * - * @param string $abstract + * @param string|callable $abstract * @param array $parameters * @return mixed * @@ -670,7 +670,7 @@ public function get($id) /** * Resolve the given type from the container. * - * @param string $abstract + * @param string|callable $abstract * @param array $parameters * @param bool $raiseEvents * @return mixed @@ -681,7 +681,7 @@ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { $abstract = $this->getAlias($abstract); - // First we wil fire any event handlers that handle the "before" resolving of + // First we'll fire any event handlers which handle the "before" resolving of // specific types. This gives some hooks the chance to add various extends // calls to change the resolution of objects that they're interested in. if ($raiseEvents) { @@ -745,7 +745,7 @@ protected function resolve($abstract, $parameters = [], $raiseEvents = true) /** * Get the concrete type for a given abstract. * - * @param string $abstract + * @param string|callable $abstract * @return mixed */ protected function getConcrete($abstract) @@ -763,7 +763,7 @@ protected function getConcrete($abstract) /** * Get the contextual concrete binding for the given abstract. * - * @param string $abstract + * @param string|callable $abstract * @return \Closure|string|array|null */ protected function getContextualConcrete($abstract) @@ -789,7 +789,7 @@ protected function getContextualConcrete($abstract) /** * Find the concrete binding for the given abstract in the contextual binding array. * - * @param string $abstract + * @param string|callable $abstract * @return \Closure|string|null */ protected function findInContextualBindings($abstract) @@ -985,10 +985,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 4f98d68d9301..03f633a07a21 100644 --- a/src/Illuminate/Contracts/Cache/Lock.php +++ b/src/Illuminate/Contracts/Cache/Lock.php @@ -17,14 +17,14 @@ public function get($callback = null); * * @param int $seconds * @param callable|null $callback - * @return bool + * @return mixed */ public function block($seconds, $callback = null); /** * Release the lock. * - * @return void + * @return bool */ public function release(); diff --git a/src/Illuminate/Contracts/Events/Dispatcher.php b/src/Illuminate/Contracts/Events/Dispatcher.php index 21be95da0ff9..638610698e68 100644 --- a/src/Illuminate/Contracts/Events/Dispatcher.php +++ b/src/Illuminate/Contracts/Events/Dispatcher.php @@ -8,7 +8,7 @@ interface Dispatcher * 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/Contracts/Queue/ShouldBeEncrypted.php b/src/Illuminate/Contracts/Queue/ShouldBeEncrypted.php new file mode 100644 index 000000000000..374df8998a0c --- /dev/null +++ b/src/Illuminate/Contracts/Queue/ShouldBeEncrypted.php @@ -0,0 +1,8 @@ +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. * @@ -147,6 +170,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/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 1dd4475290d6..d37c4a3dfb96 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Concerns; use Closure; +use RuntimeException; use Throwable; trait ManagesTransactions @@ -42,6 +43,8 @@ public function transaction(Closure $callback, $attempts = 1) try { if ($this->transactions == 1) { $this->getPdo()->commit(); + + optional($this->transactionsManager)->commit($this->getName()); } $this->transactions = max(0, $this->transactions - 1); @@ -78,6 +81,10 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma $this->transactions > 1) { $this->transactions--; + optional($this->transactionsManager)->rollback( + $this->getName(), $this->transactions + ); + throw $e; } @@ -107,6 +114,10 @@ public function beginTransaction() $this->transactions++; + optional($this->transactionsManager)->begin( + $this->getName(), $this->transactions + ); + $this->fireConnectionEvent('beganTransaction'); } @@ -176,6 +187,8 @@ public function commit() { if ($this->transactions == 1) { $this->getPdo()->commit(); + + optional($this->transactionsManager)->commit($this->getName()); } $this->transactions = max(0, $this->transactions - 1); @@ -241,6 +254,10 @@ public function rollBack($toLevel = null) $this->transactions = $toLevel; + optional($this->transactionsManager)->rollback( + $this->getName(), $this->transactions + ); + $this->fireConnectionEvent('rollingBack'); } @@ -275,6 +292,10 @@ protected function handleRollBackException(Throwable $e) { if ($this->causedByLostConnection($e)) { $this->transactions = 0; + + optional($this->transactionsManager)->rollback( + $this->getName(), $this->transactions + ); } throw $e; @@ -289,4 +310,19 @@ public function transactionLevel() { return $this->transactions; } + + /** + * Execute the callback after a transaction commits. + * + * @param callable $callback + * @return void + */ + public function afterCommit($callback) + { + if ($this->transactionsManager) { + return $this->transactionsManager->addCallback($callback); + } + + throw new RuntimeException('Transactions Manager has not been set.'); + } } diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index 98f12639713e..b4fa6d4c3a0d 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -112,6 +112,13 @@ class Connection implements ConnectionInterface */ protected $transactions = 0; + /** + * The transaction manager instance. + * + * @var \Illuminate\Database\DatabaseTransactionsManager + */ + protected $transactionsManager; + /** * Indicates if changes have been made to the database. * @@ -1151,6 +1158,29 @@ public function unsetEventDispatcher() $this->events = null; } + /** + * Set the transaction manager instance on the connection. + * + * @param \Illuminate\Database\DatabaseTransactionsManager $manager + * @return $this + */ + public function setTransactionManager($manager) + { + $this->transactionsManager = $manager; + + return $this; + } + + /** + * Unset the transaction manager for this connection. + * + * @return void + */ + public function unsetTransactionManager() + { + $this->transactionsManager = null; + } + /** * Determine if the connection is in a "dry run". * 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 4cb26bde3c3b..fe73fb2af033 100644 --- a/src/Illuminate/Database/Console/DumpCommand.php +++ b/src/Illuminate/Database/Console/DumpCommand.php @@ -32,6 +32,8 @@ class DumpCommand extends Command /** * Execute the console command. * + * @param \Illuminate\Database\ConnectionResolverInterface $connections + * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher * @return int */ public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher) @@ -64,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/DBAL/TimestampType.php b/src/Illuminate/Database/DBAL/TimestampType.php new file mode 100644 index 000000000000..0ab733cfe520 --- /dev/null +++ b/src/Illuminate/Database/DBAL/TimestampType.php @@ -0,0 +1,105 @@ +getName(); + + switch ($name) { + case 'mysql': + case 'mysql2': + return $this->getMySqlPlatformSQLDeclaration($fieldDeclaration); + + case 'postgresql': + case 'pgsql': + case 'postgres': + return $this->getPostgresPlatformSQLDeclaration($fieldDeclaration); + + case 'mssql': + return $this->getSqlServerPlatformSQLDeclaration($fieldDeclaration); + + case 'sqlite': + case 'sqlite3': + return $this->getSQLitePlatformSQLDeclaration($fieldDeclaration); + + default: + throw new DBALException('Invalid platform: '.$name); + } + } + + /** + * Get the SQL declaration for MySQL. + * + * @param array $fieldDeclaration + * @return string + */ + protected function getMySqlPlatformSQLDeclaration(array $fieldDeclaration) + { + $columnType = 'TIMESTAMP'; + + if ($fieldDeclaration['precision']) { + $columnType = 'TIMESTAMP('.$fieldDeclaration['precision'].')'; + } + + $notNull = $fieldDeclaration['notnull'] ?? false; + + if (! $notNull) { + return $columnType.' NULL'; + } + + return $columnType; + } + + /** + * Get the SQL declaration for PostgreSQL. + * + * @param array $fieldDeclaration + * @return string + */ + protected function getPostgresPlatformSQLDeclaration(array $fieldDeclaration) + { + return 'TIMESTAMP('.(int) $fieldDeclaration['precision'].')'; + } + + /** + * Get the SQL declaration for SQL Server. + * + * @param array $fieldDeclaration + * @return string + */ + protected function getSqlServerPlatformSQLDeclaration(array $fieldDeclaration) + { + return $fieldDeclaration['precision'] ?? false + ? 'DATETIME2('.$fieldDeclaration['precision'].')' + : 'DATETIME'; + } + + /** + * Get the SQL declaration for SQLite. + * + * @param array $fieldDeclaration + * @return string + */ + protected function getSQLitePlatformSQLDeclaration(array $fieldDeclaration) + { + return 'DATETIME'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'timestamp'; + } +} diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index d558d1665fc8..7f8fdf921fb3 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -174,6 +174,10 @@ protected function configure(Connection $connection, $type) $connection->setEventDispatcher($this->app['events']); } + if ($this->app->bound('db.transactions')) { + $connection->setTransactionManager($this->app['db.transactions']); + } + // Here we'll set a reconnector callback. This reconnector can be any callable // so we will set a Closure to reconnect from this manager with the name of // the connection, which will allow us to reconnect from the connections. diff --git a/src/Illuminate/Database/DatabaseServiceProvider.php b/src/Illuminate/Database/DatabaseServiceProvider.php index f64f8f2683d5..9f2ab18503e1 100755 --- a/src/Illuminate/Database/DatabaseServiceProvider.php +++ b/src/Illuminate/Database/DatabaseServiceProvider.php @@ -2,6 +2,7 @@ namespace Illuminate\Database; +use Doctrine\DBAL\Types\Type; use Faker\Factory as FakerFactory; use Faker\Generator as FakerGenerator; use Illuminate\Contracts\Queue\EntityResolver; @@ -41,10 +42,9 @@ public function register() Model::clearBootedModels(); $this->registerConnectionServices(); - $this->registerEloquentFactory(); - $this->registerQueueableEntityResolver(); + $this->registerDoctrineTypes(); } /** @@ -71,6 +71,10 @@ protected function registerConnectionServices() $this->app->bind('db.connection', function ($app) { return $app['db']->connection(); }); + + $this->app->singleton('db.transactions', function ($app) { + return new DatabaseTransactionsManager; + }); } /** @@ -104,4 +108,24 @@ protected function registerQueueableEntityResolver() return new QueueEntityResolver; }); } + + /** + * Register custom types with the Doctrine DBAL library. + * + * @return void + */ + protected function registerDoctrineTypes() + { + if (! class_exists(Type::class)) { + return; + } + + $types = $this->app['config']->get('database.dbal.types', []); + + foreach ($types as $name => $class) { + if (! Type::hasType($name)) { + Type::addType($name, $class); + } + } + } } diff --git a/src/Illuminate/Database/DatabaseTransactionRecord.php b/src/Illuminate/Database/DatabaseTransactionRecord.php new file mode 100755 index 000000000000..3259552dcfbb --- /dev/null +++ b/src/Illuminate/Database/DatabaseTransactionRecord.php @@ -0,0 +1,73 @@ +connection = $connection; + $this->level = $level; + } + + /** + * Register a callback to be executed after committing. + * + * @param callable $callback + * @return void + */ + public function addCallback($callback) + { + $this->callbacks[] = $callback; + } + + /** + * Execute all of the callbacks. + * + * @return void + */ + public function executeCallbacks() + { + foreach ($this->callbacks as $callback) { + call_user_func($callback); + } + } + + /** + * Get all of the callbacks. + * + * @return array + */ + public function getCallbacks() + { + return $this->callbacks; + } +} diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php new file mode 100755 index 000000000000..156514de6020 --- /dev/null +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -0,0 +1,96 @@ +transactions = collect(); + } + + /** + * Start a new database transaction. + * + * @param string $connection + * @param int $level + * @return void + */ + public function begin($connection, $level) + { + $this->transactions->push( + new DatabaseTransactionRecord($connection, $level) + ); + } + + /** + * Rollback the active database transaction. + * + * @param string $connection + * @param int $level + * @return void + */ + public function rollback($connection, $level) + { + $this->transactions = $this->transactions->reject(function ($transaction) use ($connection, $level) { + return $transaction->connection == $connection && + $transaction->level > $level; + })->values(); + } + + /** + * Commit the active database transaction. + * + * @param string $connection + * @return void + */ + public function commit($connection) + { + $this->transactions = $this->transactions->reject(function ($transaction) use ($connection) { + if ($transaction->connection == $connection) { + $transaction->executeCallbacks(); + + return true; + } + + return false; + })->values(); + } + + /** + * Register a transaction callback. + * + * @param callable $callback + * @return void + */ + public function addCallback($callback) + { + if ($current = $this->transactions->last()) { + return $current->addCallback($callback); + } + + call_user_func($callback); + } + + /** + * Get all the transactions. + * + * @return \Illuminate\Support\Collection + */ + public function getTransactions() + { + return $this->transactions; + } +} diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index c214c0396c84..1ecfc96140f4 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -44,9 +44,12 @@ 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', ]); } } 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..8d950e2daff4 --- /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..e96834f6e4d3 --- /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 2a3ec0ee477b..459b14c73399 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -514,11 +514,13 @@ protected function mutateAttributeForArray($key, $value) * Merge new casts with existing casts on the model. * * @param array $casts - * @return void + * @return $this */ public function mergeCasts($casts) { $this->casts = array_merge($this->casts, $casts); + + return $this; } /** @@ -777,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; } @@ -842,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] + ); } /** @@ -1512,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 049f3b294365..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 empty($class = $this->{$type}) + return is_null($class = $this->getAttributeFromArray($type)) || $class === '' ? $this->morphEagerTo($name, $type, $id, $ownerKey) : $this->morphInstanceTo($class, $name, $type, $id, $ownerKey); } @@ -731,10 +731,6 @@ public function getMorphClass() return array_search(static::class, $morphMap, true); } - if (Relation::$tableNameAsMorphType) { - return $this->getTable(); - } - return static::class; } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php index 3ede9eb66031..13ebd31744cd 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php @@ -130,7 +130,7 @@ public function getUpdatedAtColumn() /** * Get the fully qualified "created at" column. * - * @return string + * @return string|null */ public function getQualifiedCreatedAtColumn() { @@ -140,7 +140,7 @@ public function getQualifiedCreatedAtColumn() /** * Get the fully qualified "updated at" column. * - * @return string + * @return string|null */ public function getQualifiedUpdatedAtColumn() { diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 065e1b4fbcd7..4fbc2f90ebd1 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -383,8 +383,12 @@ public function withAggregate($relations, $column, $function = null) $relation = $this->getRelationWithoutConstraints($name); if ($function) { + $hashedColumn = $this->getQuery()->from === $relation->getQuery()->getQuery()->from + ? "{$relation->getRelationCountHash(false)}.$column" + : $column; + $expression = sprintf('%s(%s)', $function, $this->getQuery()->getGrammar()->wrap( - $column === '*' ? $column : $relation->getRelated()->qualifyColumn($column) + $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) )); } else { $expression = $column; diff --git a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php index 6395b28e02a2..e0c42c4c642b 100644 --- a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php @@ -10,7 +10,7 @@ class BelongsToManyRelationship /** * The related factory instance. * - * @var \Illuminate\Database\Eloquent\Factories\Factory + * @var \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ protected $factory; @@ -31,12 +31,12 @@ class BelongsToManyRelationship /** * Create a new attached relationship definition. * - * @param \Illuminate\Database\Eloquent\Factories\Factory $factory + * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model $factory * @param callable|array $pivot * @param string $relationship * @return void */ - public function __construct(Factory $factory, $pivot, $relationship) + public function __construct($factory, $pivot, $relationship) { $this->factory = $factory; $this->pivot = $pivot; @@ -51,7 +51,7 @@ public function __construct(Factory $factory, $pivot, $relationship) */ public function createFor(Model $model) { - Collection::wrap($this->factory->create([], $model))->each(function ($attachable) use ($model) { + Collection::wrap($this->factory instanceof Factory ? $this->factory->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { $model->{$this->relationship}()->attach( $attachable, is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot diff --git a/src/Illuminate/Database/Eloquent/Factories/BelongsToRelationship.php b/src/Illuminate/Database/Eloquent/Factories/BelongsToRelationship.php index 1a4ceb3c0d0b..55747fdc6488 100644 --- a/src/Illuminate/Database/Eloquent/Factories/BelongsToRelationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/BelongsToRelationship.php @@ -10,7 +10,7 @@ class BelongsToRelationship /** * The related factory instance. * - * @var \Illuminate\Database\Eloquent\Factories\Factory + * @var \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model */ protected $factory; @@ -31,11 +31,11 @@ class BelongsToRelationship /** * Create a new "belongs to" relationship definition. * - * @param \Illuminate\Database\Eloquent\Factories\Factory $factory + * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model $factory * @param string $relationship * @return void */ - public function __construct(Factory $factory, $relationship) + public function __construct($factory, $relationship) { $this->factory = $factory; $this->relationship = $relationship; @@ -52,7 +52,7 @@ public function attributesFor(Model $model) $relationship = $model->{$this->relationship}(); return $relationship instanceof MorphTo ? [ - $relationship->getMorphType() => $this->factory->newModel()->getMorphClass(), + $relationship->getMorphType() => $this->factory instanceof Factory ? $this->factory->newModel()->getMorphClass() : $this->factory->getMorphClass(), $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()), ] : [ $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()), @@ -69,7 +69,7 @@ protected function resolver($key) { return function () use ($key) { if (! $this->resolved) { - $instance = $this->factory->create(); + $instance = $this->factory instanceof Factory ? $this->factory->create() : $this->factory; return $this->resolved = $key ? $instance->{$key} : $instance->getKey(); } diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index e719683b70e2..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. @@ -105,12 +108,12 @@ abstract class Factory * Create a new factory instance. * * @param int|null $count - * @param \Illuminate\Support\Collection $states - * @param \Illuminate\Support\Collection $has - * @param \Illuminate\Support\Collection $for - * @param \Illuminate\Support\Collection $afterMaking - * @param \Illuminate\Support\Collection $afterCreating - * @param string $connection + * @param \Illuminate\Support\Collection|null $states + * @param \Illuminate\Support\Collection|null $has + * @param \Illuminate\Support\Collection|null $for + * @param \Illuminate\Support\Collection|null $afterMaking + * @param \Illuminate\Support\Collection|null $afterCreating + * @param string|null $connection * @return void */ public function __construct($count = null, @@ -481,18 +484,22 @@ protected function guessRelationship(string $related) /** * Define an attached relationship for the model. * - * @param \Illuminate\Database\Eloquent\Factories\Factory $factory + * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model $factory * @param callable|array $pivot * @param string|null $relationship * @return static */ - public function hasAttached(self $factory, $pivot = [], $relationship = null) + public function hasAttached($factory, $pivot = [], $relationship = null) { return $this->newInstance([ 'has' => $this->has->concat([new BelongsToManyRelationship( $factory, $pivot, - $relationship ?: Str::camel(Str::plural(class_basename($factory->modelName()))) + $relationship ?: Str::camel(Str::plural(class_basename( + $factory instanceof Factory + ? $factory->modelName() + : Collection::wrap($factory)->first() + ))) )]), ]); } @@ -500,15 +507,17 @@ public function hasAttached(self $factory, $pivot = [], $relationship = null) /** * Define a parent relationship for the model. * - * @param \Illuminate\Database\Eloquent\Factories\Factory $factory + * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Database\Eloquent\Model $factory * @param string|null $relationship * @return static */ - public function for(self $factory, $relationship = null) + public function for($factory, $relationship = null) { return $this->newInstance(['for' => $this->for->concat([new BelongsToRelationship( $factory, - $relationship ?: Str::camel(class_basename($factory->modelName())) + $relationship ?: Str::camel(class_basename( + $factory instanceof Factory ? $factory->modelName() : $factory + )) )])]); } @@ -741,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..575148909050 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1131,7 +1131,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 664f2b8e8049..ce7ba4421973 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php @@ -40,13 +40,6 @@ class BelongsTo extends Relation */ protected $relationName; - /** - * The count of self joins. - * - * @var int - */ - protected static $selfJoinCount = 0; - /** * Create a new belongs to relationship instance. * @@ -279,16 +272,6 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder ); } - /** - * Get a relationship join table hash. - * - * @return string - */ - public function getRelationCountHash() - { - return 'laravel_reserved_'.static::$selfJoinCount++; - } - /** * Determine if the related model has an auto-incrementing ID. * diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index b54340ec2559..9e7ed7a53a66 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -127,13 +127,6 @@ class BelongsToMany extends Relation */ protected $accessor = 'pivot'; - /** - * The count of self joins. - * - * @var int - */ - protected static $selfJoinCount = 0; - /** * Create a new belongs to many relationship instance. * @@ -564,7 +557,19 @@ public function orWherePivotNotNull($column) } /** - * Find a related model by its primary key or return new instance of the related model. + * Add an "order by" clause for a pivot table column. + * + * @param string $column + * @param string $direction + * @return $this + */ + public function orderByPivot($column, $direction = 'asc') + { + return $this->orderBy($this->qualifyPivotColumn($column), $direction); + } + + /** + * Find a related model by its primary key or return a new instance of the related model. * * @param mixed $id * @param array $columns @@ -1167,16 +1172,6 @@ public function getExistenceCompareKey() return $this->getQualifiedForeignPivotKeyName(); } - /** - * Get a relationship join table hash. - * - * @return string - */ - public function getRelationCountHash() - { - return 'laravel_reserved_'.static::$selfJoinCount++; - } - /** * Specify that the pivot table has creation and update timestamps. * diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 19bf0aee426d..117eed2f8aeb 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -123,6 +123,21 @@ public function sync($ids, $detaching = true) return $changes; } + /** + * Sync the intermediate tables with a list of IDs or collection of models with the given pivot values. + * + * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids + * @param array $values + * @param bool $detaching + * @return array + */ + public function syncWithPivotValues($ids, array $values, bool $detaching = true) + { + return $this->sync(collect($this->parseIds($ids))->mapWithKeys(function ($id) use ($values) { + return [$id => $values]; + }), $detaching); + } + /** * Format the sync / toggle record list so that it is keyed by ID. * diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index b0b568b25f01..6285cf230b7f 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -53,13 +53,6 @@ class HasManyThrough extends Relation */ protected $secondLocalKey; - /** - * The count of self joins. - * - * @var int - */ - protected static $selfJoinCount = 0; - /** * Create a new has many through relationship instance. * @@ -596,16 +589,6 @@ public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, ); } - /** - * Get a relationship join table hash. - * - * @return string - */ - public function getRelationCountHash() - { - return 'laravel_reserved_'.static::$selfJoinCount++; - } - /** * Get the qualified foreign key on the related model. * diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index acff8314640e..f09133c8bf6c 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -22,13 +22,6 @@ abstract class HasOneOrMany extends Relation */ protected $localKey; - /** - * The count of self joins. - * - * @var int - */ - protected static $selfJoinCount = 0; - /** * Create a new has one or many relationship instance. * @@ -60,7 +53,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 @@ -189,7 +182,7 @@ protected function buildDictionary(Collection $results) } /** - * 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 @@ -363,16 +356,6 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder ); } - /** - * Get a relationship join table hash. - * - * @return string - */ - public function getRelationCountHash() - { - return 'laravel_reserved_'.static::$selfJoinCount++; - } - /** * Get the key for comparing against the parent key in "has" query. * diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index ebcba4d845b7..8a6c7a2cbca8 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -164,7 +164,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]))); } /** @@ -226,7 +226,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,9 +324,9 @@ 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 $with + * @param array $callbacks * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ public function constrain(array $callbacks) diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 97ee55de772b..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,18 +51,18 @@ 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 */ public static $morphMap = []; /** - * Indicates if the morph relation type should default to table name. + * The count of self joins. * - * @var bool + * @var int */ - public static $tableNameAsMorphType = false; + protected static $selfJoinCount = 0; /** * Create a new relation instance. @@ -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. * @@ -220,6 +246,17 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, ); } + /** + * Get a relationship join table hash. + * + * @param bool $incrementJoinCount + * @return string + */ + public function getRelationCountHash($incrementJoinCount = true) + { + return 'laravel_reserved_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount); + } + /** * Get all of the primary keys for an array of models. * @@ -348,16 +385,6 @@ public static function morphMap(array $map = null, $merge = true) return static::$morphMap; } - /** - * Specifies that the morph types should be table names. - * - * @return void - */ - public static function tableNameAsMorphType() - { - self::$tableNameAsMorphType = true; - } - /** * Builds a table-keyed array from model class names. * 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/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 801033047608..17b1aff01347 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -17,10 +17,9 @@ class MySqlGrammar extends Grammar /** * Add a "where null" clause to the query. * - * @param string|array $columns - * @param string $boolean - * @param bool $not - * @return $this + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string */ protected function whereNull(Builder $query, $where) { @@ -36,9 +35,9 @@ protected function whereNull(Builder $query, $where) /** * Add a "where not null" clause to the query. * - * @param string|array $columns - * @param string $boolean - * @return $this + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string */ protected function whereNotNull(Builder $query, $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..68b6814a83cb 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, ])); } 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/SchemaState.php b/src/Illuminate/Database/Schema/SchemaState.php index 6c4e56578252..5629a7aa303e 100644 --- a/src/Illuminate/Database/Schema/SchemaState.php +++ b/src/Illuminate/Database/Schema/SchemaState.php @@ -47,8 +47,8 @@ abstract class SchemaState * Create a new dumper instance. * * @param \Illuminate\Database\Connection $connection - * @param \Illuminate\Filesystem\Filesystem $files - * @param callable $processFactory + * @param \Illuminate\Filesystem\Filesystem|null $files + * @param callable|null $processFactory * @return void */ public function __construct(Connection $connection, Filesystem $files = null, callable $processFactory = null) 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/Schema/SqliteSchemaState.php b/src/Illuminate/Database/Schema/SqliteSchemaState.php index 34b8659a8eb2..42652baa420d 100644 --- a/src/Illuminate/Database/Schema/SqliteSchemaState.php +++ b/src/Illuminate/Database/Schema/SqliteSchemaState.php @@ -34,6 +34,7 @@ public function dump(Connection $connection, $path) /** * Append the migration data to the schema dump. * + * @param string $path * @return void */ protected function appendMigrationData(string $path) @@ -80,6 +81,7 @@ protected function baseCommand() /** * Get the base variables for a dump / load command. * + * @param array $config * @return array */ protected function baseVariables(array $config) 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/Database/composer.json b/src/Illuminate/Database/composer.json index f04ea32227cb..0a7cda072a90 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -22,7 +22,7 @@ "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0", - "symfony/console": "^5.1" + "symfony/console": "^5.1.4" }, "autoload": { "psr-4": { @@ -41,7 +41,7 @@ "illuminate/events": "Required to use the observers with Eloquent (^8.0).", "illuminate/filesystem": "Required to use the migrations (^8.0).", "illuminate/pagination": "Required to paginate the result set (^8.0).", - "symfony/finder": "Required to use Eloquent model factories (^5.1)." + "symfony/finder": "Required to use Eloquent model factories (^5.1.4)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Encryption/MissingAppKeyException.php b/src/Illuminate/Encryption/MissingAppKeyException.php index 4f799dad9e9b..d8ffcd184b51 100644 --- a/src/Illuminate/Encryption/MissingAppKeyException.php +++ b/src/Illuminate/Encryption/MissingAppKeyException.php @@ -9,6 +9,7 @@ class MissingAppKeyException extends RuntimeException /** * Create a new exception instance. * + * @param string $message * @return void */ public function __construct($message = 'No application encryption key has been specified.') 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 91214b457188..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 @@ -432,7 +433,11 @@ protected function createClassCallable($listener) return $this->createQueuedHandlerCallable($class, $method); } - return [$this->container->make($class), $method]; + $listener = $this->container->make($class); + + return $this->handlerShouldBeDispatchedAfterDatabaseTransactions($listener) + ? $this->createCallbackForListenerRunningAfterCommits($listener, $method) + : [$listener, $method]; } /** @@ -483,6 +488,37 @@ protected function createQueuedHandlerCallable($class, $method) }; } + /** + * Determine if the given event handler should be dispatched after all database transactions have committed. + * + * @param object|mixed $listener + * @return bool + */ + protected function handlerShouldBeDispatchedAfterDatabaseTransactions($listener) + { + return ($listener->afterCommit ?? null) && $this->container->bound('db.transactions'); + } + + /** + * Create a callable for dispatching a listener after database transactions. + * + * @param mixed $listener + * @param string $method + * @return \Closure + */ + protected function createCallbackForListenerRunningAfterCommits($listener, $method) + { + return function () use ($method, $listener) { + $payload = func_get_args(); + + $this->container->make('db.transactions')->addCallback( + function () use ($listener, $method, $payload) { + $listener->$method(...$payload); + } + ); + }; + } + /** * Determine if the event handler wants to be queued. * @@ -554,11 +590,19 @@ protected function propagateListenerOptions($listener, $job) { return tap($job, function ($job) use ($listener) { $job->tries = $listener->tries ?? null; + $job->backoff = method_exists($listener, 'backoff') ? $listener->backoff() : ($listener->backoff ?? null); + $job->timeout = $listener->timeout ?? null; + + $job->afterCommit = property_exists($listener, 'afterCommit') + ? $listener->afterCommit : null; + $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..5c020cfdd732 100644 --- a/src/Illuminate/Events/NullDispatcher.php +++ b/src/Illuminate/Events/NullDispatcher.php @@ -65,7 +65,7 @@ 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..896bfbd6f35a 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -20,6 +20,7 @@ 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 RuntimeException; @@ -449,7 +450,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); diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index f9d47250d08f..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); } /** @@ -326,7 +326,7 @@ public function getDefaultDriver() */ public function getDefaultCloudDriver() { - return $this->app['config']['filesystems.cloud']; + return $this->app['config']['filesystems.cloud'] ?? 's3'; } /** diff --git a/src/Illuminate/Filesystem/composer.json b/src/Illuminate/Filesystem/composer.json index 271f6a5135dc..16cb3b6d2edc 100644 --- a/src/Illuminate/Filesystem/composer.json +++ b/src/Illuminate/Filesystem/composer.json @@ -19,7 +19,7 @@ "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0", - "symfony/finder": "^5.1" + "symfony/finder": "^5.1.4" }, "autoload": { "psr-4": { @@ -39,8 +39,8 @@ "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^5.1).", - "symfony/mime": "Required to enable support for guessing extensions (^5.1)." + "symfony/filesystem": "Required to enable support for relative symbolic links (^5.1.4).", + "symfony/mime": "Required to enable support for guessing extensions (^5.1.4)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index ac87f53de5b5..45be0dd0b96e 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.16.1'; + const VERSION = '8.31.0'; /** * 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 @@ -1311,8 +1356,11 @@ public function flush() $this->serviceProviders = []; $this->resolvingCallbacks = []; $this->terminatingCallbacks = []; + $this->beforeResolvingCallbacks = []; $this->afterResolvingCallbacks = []; + $this->globalBeforeResolvingCallbacks = []; $this->globalResolvingCallbacks = []; + $this->globalAfterResolvingCallbacks = []; } /** diff --git a/src/Illuminate/Foundation/Bus/Dispatchable.php b/src/Illuminate/Foundation/Bus/Dispatchable.php index 403c3d67e91e..1a0e99dc8833 100644 --- a/src/Illuminate/Foundation/Bus/Dispatchable.php +++ b/src/Illuminate/Foundation/Bus/Dispatchable.php @@ -21,6 +21,7 @@ public static function dispatch() * Dispatch the job with the given arguments if the given truth test passes. * * @param bool $boolean + * @param mixed ...$arguments * @return \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent */ public static function dispatchIf($boolean, ...$arguments) @@ -34,6 +35,7 @@ public static function dispatchIf($boolean, ...$arguments) * Dispatch the job with the given arguments unless the given truth test passes. * * @param bool $boolean + * @param mixed ...$arguments * @return \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent */ public static function dispatchUnless($boolean, ...$arguments) diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index 9495dc15c114..8ac4432319a9 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -99,6 +99,30 @@ public function delay($delay) return $this; } + /** + * Indicate that the job should be dispatched after all database transactions have committed. + * + * @return $this + */ + public function afterCommit() + { + $this->job->afterCommit(); + + return $this; + } + + /** + * Indicate that the job should not wait until database transactions have been committed before dispatching. + * + * @return $this + */ + public function beforeCommit() + { + $this->job->beforeCommit(); + + return $this; + } + /** * Set the jobs that should run if this job is successful. * diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index 94c5906531d6..feaf95047078 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -100,7 +100,9 @@ protected function prerenderView() { (new RegisterErrorViewPaths)(); - return view($this->option('render'))->render(); + return view($this->option('render'), [ + 'retryAfter' => $this->option('retry'), + ])->render(); } /** diff --git a/src/Illuminate/Foundation/Console/Kernel.php b/src/Illuminate/Foundation/Console/Kernel.php index 9c7c576ba8cd..6a8eeb0c86f9 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -111,7 +111,7 @@ protected function defineConsoleSchedule() */ protected function scheduleCache() { - return Env::get('SCHEDULE_CACHE_DRIVER'); + return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER')); } /** 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..46fbf5ff6256 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 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/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/Events/Dispatchable.php b/src/Illuminate/Foundation/Events/Dispatchable.php index c2acd7759b22..ff633150f911 100644 --- a/src/Illuminate/Foundation/Events/Dispatchable.php +++ b/src/Illuminate/Foundation/Events/Dispatchable.php @@ -18,6 +18,7 @@ public static function dispatch() * Dispatch the event with the given arguments if the given truth test passes. * * @param bool $boolean + * @param mixed ...$arguments * @return void */ public static function dispatchIf($boolean, ...$arguments) @@ -31,6 +32,7 @@ public static function dispatchIf($boolean, ...$arguments) * Dispatch the event with the given arguments unless the given truth test passes. * * @param bool $boolean + * @param mixed ...$arguments * @return void */ public static function dispatchUnless($boolean, ...$arguments) 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..38c29a3996c7 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', ]; @@ -274,6 +278,10 @@ protected function shouldntReport(Throwable $e) */ protected function exceptionContext(Throwable $e) { + if (method_exists($e, 'context')) { + return $e->context(); + } + return []; } @@ -369,6 +377,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/Exceptions/views/illustrated-layout.blade.php b/src/Illuminate/Foundation/Exceptions/views/illustrated-layout.blade.php index 64eb7cbb8bd5..2e5b8240b59b 100644 --- a/src/Illuminate/Foundation/Exceptions/views/illustrated-layout.blade.php +++ b/src/Illuminate/Foundation/Exceptions/views/illustrated-layout.blade.php @@ -7,8 +7,8 @@ @yield('title') - - + + 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..ea64ce94255c 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 diff --git a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php index ffd65b399b2a..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 \Illuminate\Foundation\Http\Exceptions\MaintenanceModeException */ 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/Http/Middleware/VerifyCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php index 6a1f028f9ce8..59483200e4d0 100644 --- a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php +++ b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php @@ -3,6 +3,7 @@ namespace Illuminate\Foundation\Http\Middleware; use Closure; +use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Support\Responsable; @@ -152,7 +153,11 @@ protected function getTokenFromRequest($request) $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN'); if (! $token && $header = $request->header('X-XSRF-TOKEN')) { - $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized())); + try { + $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized())); + } catch (DecryptException $e) { + $token = ''; + } } return $token; diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index c4d31671075d..4426e4b5d34c 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -7,7 +7,9 @@ use Illuminate\Cache\Console\ClearCommand as CacheClearCommand; use Illuminate\Cache\Console\ForgetCommand as CacheForgetCommand; use Illuminate\Console\Scheduling\ScheduleFinishCommand; +use Illuminate\Console\Scheduling\ScheduleListCommand; use Illuminate\Console\Scheduling\ScheduleRunCommand; +use Illuminate\Console\Scheduling\ScheduleTestCommand; use Illuminate\Console\Scheduling\ScheduleWorkCommand; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Database\Console\DbCommand; @@ -65,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; @@ -105,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', @@ -115,7 +119,9 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'SchemaDump' => 'command.schema.dump', 'Seed' => 'command.seed', 'ScheduleFinish' => ScheduleFinishCommand::class, + 'ScheduleList' => ScheduleListCommand::class, 'ScheduleRun' => ScheduleRunCommand::class, + 'ScheduleTest' => ScheduleTestCommand::class, 'ScheduleWork' => ScheduleWorkCommand::class, 'StorageLink' => 'command.storage.link', 'Up' => 'command.up', @@ -680,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. * @@ -918,6 +936,16 @@ protected function registerScheduleFinishCommand() $this->app->singleton(ScheduleFinishCommand::class); } + /** + * Register the command. + * + * @return void + */ + protected function registerScheduleListCommand() + { + $this->app->singleton(ScheduleListCommand::class); + } + /** * Register the command. * @@ -928,6 +956,16 @@ protected function registerScheduleRunCommand() $this->app->singleton(ScheduleRunCommand::class); } + /** + * Register the command. + * + * @return void + */ + protected function registerScheduleTestCommand() + { + $this->app->singleton(ScheduleTestCommand::class); + } + /** * 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/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/InteractsWithRedis.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php index a68995b05a9d..1c679fb60197 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php @@ -57,6 +57,7 @@ public function setUpRedis() 'port' => $port, 'database' => 5, 'timeout' => 0.5, + 'name' => 'default', ], ]); } diff --git a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php index 10e55aab1358..0be549f99cc2 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -508,12 +508,12 @@ public function call($method, $uri, $parameters = [], $cookies = [], $files = [] $request = Request::createFromBase($symfonyRequest) ); + $kernel->terminate($request, $response); + if ($this->followRedirects) { $response = $this->followRedirects($response); } - $kernel->terminate($request, $response); - return $this->createTestResponse($response); } @@ -623,12 +623,12 @@ protected function prepareCookiesForJsonRequest() */ protected function followRedirects($response) { + $this->followRedirects = false; + while ($response->isRedirect()) { $response = $this->get($response->headers->get('Location')); } - $this->followRedirects = false; - return $response; } 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/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 7d8911605f51..0f182b6a65cb 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -224,6 +224,28 @@ public function assertSent($callback) ); } + /** + * Assert that the given request was sent in the given order. + * + * @param array $callbacks + * @return void + */ + public function assertSentInOrder($callbacks) + { + $this->assertSentCount(count($callbacks)); + + foreach ($callbacks as $index => $url) { + $callback = is_callable($url) ? $url : function ($request) use ($url) { + return $request->url() == $url; + }; + + PHPUnit::assertTrue($callback( + $this->recorded[$index][0], + $this->recorded[$index][1] + ), 'An expected request (#'.($index + 1).') was not recorded.'); + } + } + /** * Assert that a request / response pair was not recorded matching a given truth test. * diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index eb1db35babe3..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. * @@ -205,6 +217,16 @@ public function cookies() return $this->cookies; } + /** + * Get the handler stats of the response. + * + * @return array + */ + public function handlerStats() + { + return $this->transferStats->getHandlerStats(); + } + /** * Get the underlying PSR response for the response. * diff --git a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php index be760a2619d9..faf25d92e081 100644 --- a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php +++ b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php @@ -6,24 +6,6 @@ trait InteractsWithContentTypes { - /** - * Determine if the given content types match. - * - * @param string $actual - * @param string $type - * @return bool - */ - public static function matchesType($actual, $type) - { - if ($actual === $type) { - return true; - } - - $split = explode('/', $actual); - - return isset($split[1]) && preg_match('#'.preg_quote($split[0], '#').'/.+\+'.preg_quote($split[1], '#').'#', $type); - } - /** * Determine if the request is sending JSON. * @@ -31,7 +13,7 @@ public static function matchesType($actual, $type) */ public function isJson() { - return Str::contains($this->header('CONTENT_TYPE'), ['/json', '+json']); + return Str::contains($this->header('CONTENT_TYPE') ?? '', ['/json', '+json']); } /** @@ -152,6 +134,24 @@ public function acceptsHtml() return $this->accepts('text/html'); } + /** + * Determine if the given content types match. + * + * @param string $actual + * @param string $type + * @return bool + */ + public static function matchesType($actual, $type) + { + if ($actual === $type) { + return true; + } + + $split = explode('/', $actual); + + return isset($split[1]) && preg_match('#'.preg_quote($split[0], '#').'/.+\+'.preg_quote($split[1], '#').'#', $type); + } + /** * Get the data format expected in the response. * diff --git a/src/Illuminate/Http/Concerns/InteractsWithInput.php b/src/Illuminate/Http/Concerns/InteractsWithInput.php index 4550271b0f61..69b00672de88 100644 --- a/src/Illuminate/Http/Concerns/InteractsWithInput.php +++ b/src/Illuminate/Http/Concerns/InteractsWithInput.php @@ -7,6 +7,7 @@ use Illuminate\Support\Str; use SplFileInfo; use stdClass; +use Symfony\Component\VarDumper\VarDumper; trait InteractsWithInput { @@ -462,4 +463,34 @@ protected function retrieveItem($source, $key, $default) return $this->$source->get($key, $default); } + + /** + * Dump the request items and end the script. + * + * @param array|mixed $keys + * @return void + */ + public function dd(...$keys) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + call_user_func_array([$this, 'dump'], $keys); + + exit(1); + } + + /** + * Dump the items. + * + * @param array $keys + * @return $this + */ + public function dump($keys = []) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + VarDumper::dump(count($keys) > 0 ? $this->only($keys) : $this->all()); + + return $this; + } } diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index cf6b90cb1da0..068cf23790da 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -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/composer.json b/src/Illuminate/Http/composer.json index a01a5d86c436..8bf355434520 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -20,9 +20,9 @@ "illuminate/macroable": "^8.0", "illuminate/session": "^8.0", "illuminate/support": "^8.0", - "symfony/http-foundation": "^5.1", - "symfony/http-kernel": "^5.1", - "symfony/mime": "^5.1" + "symfony/http-foundation": "^5.1.4", + "symfony/http-kernel": "^5.1.4", + "symfony/mime": "^5.1.4" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index a8a4a291d0c9..97fcda7827c5 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -327,8 +327,13 @@ 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()); }); diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 6876ba4878dd..1fe0b7ea3d20 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -7,12 +7,14 @@ use Illuminate\Contracts\Mail\Factory as MailFactory; use Illuminate\Contracts\Mail\Mailable as MailableContract; use Illuminate\Contracts\Queue\Factory as Queue; +use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\Localizable; +use PHPUnit\Framework\Assert as PHPUnit; use ReflectionClass; use ReflectionProperty; @@ -146,6 +148,13 @@ class Mailable implements MailableContract, Renderable */ public $mailer; + /** + * The rendered mailable views for testing / assertions. + * + * @var array + */ + protected $assertionableRenderStrings; + /** * The callback that should be invoked while building the view data. * @@ -844,6 +853,106 @@ public function attachData($data, $name, array $options = []) return $this; } + /** + * Assert that the given text is present in the HTML email body. + * + * @param string $string + * @return void + */ + public function assertSeeInHtml($string) + { + [$html, $text] = $this->renderForAssertions(); + + PHPUnit::assertTrue( + Str::contains($html, $string), + "Did not see expected text [{$string}] within email body." + ); + } + + /** + * Assert that the given text is not present in the HTML email body. + * + * @param string $string + * @return void + */ + public function assertDontSeeInHtml($string) + { + [$html, $text] = $this->renderForAssertions(); + + PHPUnit::assertFalse( + Str::contains($html, $string), + "Saw unexpected text [{$string}] within email body." + ); + } + + /** + * Assert that the given text is present in the plain-text email body. + * + * @param string $string + * @return void + */ + public function assertSeeInText($string) + { + [$html, $text] = $this->renderForAssertions(); + + PHPUnit::assertTrue( + Str::contains($text, $string), + "Did not see expected text [{$string}] within text email body." + ); + } + + /** + * Assert that the given text is not present in the plain-text email body. + * + * @param string $string + * @return void + */ + public function assertDontSeeInText($string) + { + [$html, $text] = $this->renderForAssertions(); + + PHPUnit::assertFalse( + Str::contains($text, $string), + "Saw unexpected text [{$string}] within text email body." + ); + } + + /** + * Render the HTML and plain-text version of the mailable into views for assertions. + * + * @return array + * + * @throws \ReflectionException + */ + protected function renderForAssertions() + { + if ($this->assertionableRenderStrings) { + return $this->assertionableRenderStrings; + } + + return $this->assertionableRenderStrings = $this->withLocale($this->locale, function () { + Container::getInstance()->call([$this, 'build']); + + $html = Container::getInstance()->make('mailer')->render( + $view = $this->buildView(), $this->buildViewData() + ); + + if (is_array($view) && isset($view[1])) { + $text = $view[1]; + } + + $text = $text ?? $view['text'] ?? ''; + + if (! empty($text) && ! $text instanceof Htmlable) { + $text = Container::getInstance()->make('mailer')->render( + $text, $this->buildViewData() + ); + } + + return [(string) $html, (string) $text]; + }); + } + /** * Set the name of the mailer that should send the message. * diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 1a96bbc54ddf..668d68baaf2a 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -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)) { @@ -398,7 +398,7 @@ protected function setGlobalToAndRemoveCcAndBcc($message) /** * Queue a new e-mail message for sending. * - * @param \Illuminate\Contracts\Mail\Mailable $view + * @param \Illuminate\Contracts\Mail\Mailable|string|array $view * @param string|null $queue * @return mixed * 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 d9ac1130c482..43c30961f90b 100644 --- a/src/Illuminate/Mail/PendingMail.php +++ b/src/Illuminate/Mail/PendingMail.php @@ -114,7 +114,6 @@ public function bcc($users) * Send a new mailable message instance. * * @param \Illuminate\Contracts\Mail\Mailable $mailable - * * @return mixed */ public function send(MailableContract $mailable) diff --git a/src/Illuminate/Mail/SendQueuedMailable.php b/src/Illuminate/Mail/SendQueuedMailable.php index 76822bcd05f5..1009789b4bf0 100644 --- a/src/Illuminate/Mail/SendQueuedMailable.php +++ b/src/Illuminate/Mail/SendQueuedMailable.php @@ -2,11 +2,15 @@ namespace Illuminate\Mail; +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 { + use Queueable; + /** * The mailable message instance. * @@ -28,6 +32,13 @@ class SendQueuedMailable */ public $timeout; + /** + * Indicates if the job should be encrypted. + * + * @var bool + */ + public $shouldBeEncrypted = false; + /** * Create a new job instance. * @@ -39,6 +50,8 @@ public function __construct(MailableContract $mailable) $this->mailable = $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; } /** @@ -76,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/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 98fb2fe56d9a..39be0e598796 100644 --- a/src/Illuminate/Notifications/NotificationSender.php +++ b/src/Illuminate/Notifications/NotificationSender.php @@ -171,7 +171,7 @@ protected function shouldSendNotification($notifiable, $notification, $channel) * Queue the given notification instances. * * @param mixed $notifiables - * @param array[\Illuminate\Notifications\Channels\Notification] $notification + * @param \Illuminate\Notifications\Notification $notification * @return void */ protected function queueNotification($notifiables, $notification) diff --git a/src/Illuminate/Notifications/SendQueuedNotifications.php b/src/Illuminate/Notifications/SendQueuedNotifications.php index bab695284725..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. * @@ -64,6 +72,8 @@ public function __construct($notifiables, $notification, array $channels = null) $this->notifiables = $this->wrapNotifiables($notifiables); $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; } /** @@ -118,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/resources/views/simple-tailwind.blade.php b/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php index 1c5e52f3e52a..6872cca360d5 100644 --- a/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php +++ b/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php @@ -6,14 +6,14 @@ {!! __('pagination.previous') !!} @else - @endif {{-- Next Page Link --}} @if ($paginator->hasMorePages()) - @else diff --git a/src/Illuminate/Pagination/resources/views/tailwind.blade.php b/src/Illuminate/Pagination/resources/views/tailwind.blade.php index 2f5eca93e152..2dd4d0ef3389 100644 --- a/src/Illuminate/Pagination/resources/views/tailwind.blade.php +++ b/src/Illuminate/Pagination/resources/views/tailwind.blade.php @@ -6,13 +6,13 @@ {!! __('pagination.previous') !!} @else - + {!! __('pagination.previous') !!} @endif @if ($paginator->hasMorePages()) - + {!! __('pagination.next') !!} @else @@ -47,7 +47,7 @@ @else - + {{ $page }} @endif @@ -81,7 +81,7 @@ {{-- Next Page Link --}} @if ($paginator->hasMorePages()) - 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/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/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 78a6792274a5..35e5e64d2ea0 100644 --- a/tests/Redis/ConcurrentLimiterTest.php +++ b/tests/Redis/ConcurrentLimiterTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Redis; +use Error; use Illuminate\Contracts\Redis\LimiterTimeoutException; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Redis\Limiters\ConcurrencyLimiter; @@ -140,6 +141,27 @@ public function testItFailsAfterRetryTimeout() $this->assertEquals([1], $store); } + public function testItReleasesIfErrorIsThrown() + { + $store = []; + + $lock = new ConcurrencyLimiter($this->redis(), 'key', 1, 5); + + try { + $lock->block(1, function () { + throw new Error(); + }); + } catch (Error $e) { + } + + $lock = new ConcurrencyLimiter($this->redis(), 'key', 1, 5); + $lock->block(1, function () use (&$store) { + $store[] = 1; + }); + + $this->assertEquals([1], $store); + } + private function redis() { return $this->redis['phpredis']->connection(); 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 36b488582215..965ef46eb69a 100644 --- a/tests/Redis/RedisConnectorTest.php +++ b/tests/Redis/RedisConnectorTest.php @@ -41,6 +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')); } public function testUrl() @@ -160,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..650b118ec646 100644 --- a/tests/Redis/RedisManagerExtensionTest.php +++ b/tests/Redis/RedisManagerExtensionTest.php @@ -39,7 +39,7 @@ protected function setUp(): void ]); $this->redis->extend('my_custom_driver', function () { - return new FakeRedisConnnector(); + return new FakeRedisConnector(); }); } @@ -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/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..8c562d572d48 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']); @@ -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])); } /** @@ -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->assertEquals('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->assertEquals('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..af7de6b40f56 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; @@ -362,10 +363,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/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/SupportStrTest.php b/tests/Support/SupportStrTest.php index 87bc3c0956c4..f873e3bd934d 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -511,6 +511,12 @@ public function invalidUuidList() ['ff6f8cb0-c57da-51e1-9b21-0800200c9a66'], ]; } + + public function testMarkdown() + { + $this->assertEquals("

hello world

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

hello world

\n", Str::markdown('# hello world')); + } } class StringableObjectStub diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 1986d050de9c..d6c010fd20fd 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()); @@ -545,4 +553,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/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/ParallelConsoleOutputTest.php b/tests/Testing/ParallelConsoleOutputTest.php new file mode 100644 index 000000000000..1543be66aee3 --- /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..d7a0406019ed --- /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/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 8aae14243652..42d16e72d3fa 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1,9 +1,12 @@ 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']; @@ -1105,6 +1130,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 () { @@ -1154,6 +1204,79 @@ public function testItCanBeTapped() })->assertStatus(418); } + public function testAssertPlainCookie() + { + $response = TestResponse::fromBaseResponse( + (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value')) + ); + + $response->assertPlainCookie('cookie-name', 'cookie-value'); + } + + public function testAssertCookie() + { + $container = Container::getInstance(); + $encrypter = new Encrypter(str_repeat('a', 16)); + $container->singleton('encrypter', function () use ($encrypter) { + return $encrypter; + }); + + $cookieName = 'cookie-name'; + $cookieValue = 'cookie-value'; + $encryptedValue = $encrypter->encrypt(CookieValuePrefix::create($cookieName, $encrypter->getKey()).$cookieValue, false); + + $response = TestResponse::fromBaseResponse( + (new Response())->withCookie(new Cookie($cookieName, $encryptedValue)) + ); + + $response->assertCookie($cookieName, $cookieValue); + } + + public function testAssertCookieExpired() + { + $response = TestResponse::fromBaseResponse( + (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', time() - 5000)) + ); + + $response->assertCookieExpired('cookie-name'); + } + + public function testAssertSessionCookieExpiredDoesNotTriggerOnSessionCookies() + { + $response = TestResponse::fromBaseResponse( + (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', 0)) + ); + + $this->expectException(ExpectationFailedException::class); + + $response->assertCookieExpired('cookie-name'); + } + + public function testAssertCookieNotExpired() + { + $response = TestResponse::fromBaseResponse( + (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', time() + 5000)) + ); + + $response->assertCookieNotExpired('cookie-name'); + } + + public function testAssertSessionCookieNotExpired() + { + $response = TestResponse::fromBaseResponse( + (new Response())->withCookie(new Cookie('cookie-name', 'cookie-value', 0)) + ); + + $response->assertCookieNotExpired('cookie-name'); + } + + public function testAssertCookieMissing() + { + $response = TestResponse::fromBaseResponse(new Response()); + + $response->assertCookieMissing('cookie-name'); + } + private function makeMockResponse($content) { $baseResponse = tap(new Response, function ($response) use ($content) { diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 9b2a96d3591e..a60b93b9abf4 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1553,6 +1553,10 @@ public function testValidateJson() $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['foo' => '{"name":"John","age":"34"}'], ['foo' => 'json']); $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => ['array']], ['foo' => 'json']); + $this->assertFalse($v->passes()); } public function testValidateBoolean() @@ -1830,14 +1834,14 @@ public function testValidateMax() $this->assertFalse($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['isValid', 'getSize'])->setConstructorArgs([__FILE__, basename(__FILE__)])->getMock(); - $file->expects($this->any())->method('isValid')->willReturn(true); - $file->expects($this->once())->method('getSize')->willReturn(3072); + $file->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(3072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Max:10']); $this->assertTrue($v->passes()); $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['isValid', 'getSize'])->setConstructorArgs([__FILE__, basename(__FILE__)])->getMock(); - $file->expects($this->any())->method('isValid')->willReturn(true); - $file->expects($this->once())->method('getSize')->willReturn(4072); + $file->method('isValid')->willReturn(true); + $file->method('getSize')->willReturn(4072); $v = new Validator($trans, ['photo' => $file], ['photo' => 'Max:2']); $this->assertFalse($v->passes()); @@ -2833,44 +2837,63 @@ public function testValidateImage() $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file->expects($this->any())->method('guessExtension')->willReturn('php'); $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('php'); - $v = new Validator($trans, ['x' => $file], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file], ['x' => 'image']); $this->assertFalse($v->passes()); $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file2->expects($this->any())->method('guessExtension')->willReturn('jpeg'); $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpeg'); - $v = new Validator($trans, ['x' => $file2], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file2], ['x' => 'image']); + $this->assertTrue($v->passes()); + + $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); + $file2->expects($this->any())->method('guessExtension')->willReturn('jpg'); + $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $v = new Validator($trans, ['x' => $file2], ['x' => 'image']); + $this->assertTrue($v->passes()); + + $file2 = $this->getMockBuilder(UploadedFile::class)->setMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); + $file2->expects($this->any())->method('guessExtension')->willReturn('jpg'); + $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $v = new Validator($trans, ['x' => $file2], ['x' => 'image']); $this->assertTrue($v->passes()); $file3 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file3->expects($this->any())->method('guessExtension')->willReturn('gif'); $file3->expects($this->any())->method('getClientOriginalExtension')->willReturn('gif'); - $v = new Validator($trans, ['x' => $file3], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file3], ['x' => 'image']); $this->assertTrue($v->passes()); $file4 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file4->expects($this->any())->method('guessExtension')->willReturn('bmp'); $file4->expects($this->any())->method('getClientOriginalExtension')->willReturn('bmp'); - $v = new Validator($trans, ['x' => $file4], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file4], ['x' => 'image']); $this->assertTrue($v->passes()); $file5 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file5->expects($this->any())->method('guessExtension')->willReturn('png'); $file5->expects($this->any())->method('getClientOriginalExtension')->willReturn('png'); - $v = new Validator($trans, ['x' => $file5], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file5], ['x' => 'image']); $this->assertTrue($v->passes()); $file6 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file6->expects($this->any())->method('guessExtension')->willReturn('svg'); $file6->expects($this->any())->method('getClientOriginalExtension')->willReturn('svg'); - $v = new Validator($trans, ['x' => $file6], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file6], ['x' => 'image']); $this->assertTrue($v->passes()); $file7 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file7->expects($this->any())->method('guessExtension')->willReturn('webp'); $file7->expects($this->any())->method('getClientOriginalExtension')->willReturn('webp'); + $v = new Validator($trans, ['x' => $file7], ['x' => 'Image']); $this->assertTrue($v->passes()); + + $file2 = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); + $file2->expects($this->any())->method('guessExtension')->willReturn('jpg'); + $file2->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $v = new Validator($trans, ['x' => $file2], ['x' => 'Image']); + $this->assertTrue($v->passes()); } public function testValidateImageDoesNotAllowPhpExtensionsOnImageMime() @@ -2881,7 +2904,7 @@ public function testValidateImageDoesNotAllowPhpExtensionsOnImageMime() $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file->expects($this->any())->method('guessExtension')->willReturn('jpeg'); $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('php'); - $v = new Validator($trans, ['x' => $file], ['x' => 'Image']); + $v = new Validator($trans, ['x' => $file], ['x' => 'image']); $this->assertFalse($v->passes()); } @@ -2959,7 +2982,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(); @@ -2972,7 +2995,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(); @@ -2994,20 +3017,30 @@ public function testValidateImageDimensions() $this->assertFalse($v->passes()); } - /** - * @requires extension fileinfo - */ - public function testValidatePhpMimetypes() + public function testValidateMimetypes() { $trans = $this->getIlluminateArrayTranslator(); + $uploadedFile = [__DIR__.'/ValidationRuleTest.php', '', null, null, true]; $file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); $file->expects($this->any())->method('guessExtension')->willReturn('rtf'); $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('rtf'); + $file = $this->getMockBuilder(UploadedFile::class)->setMethods(['getMimeType'])->setConstructorArgs($uploadedFile)->getMock(); + $file->expects($this->any())->method('getMimeType')->willReturn('text/rtf'); $v = new Validator($trans, ['x' => $file], ['x' => 'mimetypes:text/*']); $this->assertTrue($v->passes()); + + $file = $this->getMockBuilder(UploadedFile::class)->setMethods(['getMimeType'])->setConstructorArgs($uploadedFile)->getMock(); + $file->expects($this->any())->method('getMimeType')->willReturn('application/pdf'); + $v = new Validator($trans, ['x' => $file], ['x' => 'mimetypes:text/rtf']); + $this->assertFalse($v->passes()); + + $file = $this->getMockBuilder(UploadedFile::class)->setMethods(['getMimeType'])->setConstructorArgs($uploadedFile)->getMock(); + $file->expects($this->any())->method('getMimeType')->willReturn('image/jpeg'); + $v = new Validator($trans, ['x' => $file], ['x' => 'mimetypes:image/jpeg']); + $this->assertTrue($v->passes()); } public function testValidateMime() @@ -3026,6 +3059,18 @@ public function testValidateMime() $file2->expects($this->any())->method('isValid')->willReturn(false); $v = new Validator($trans, ['x' => $file2], ['x' => 'mimes:pdf']); $this->assertFalse($v->passes()); + + $file = $this->getMockBuilder(UploadedFile::class)->setMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); + $file->expects($this->any())->method('guessExtension')->willReturn('jpg'); + $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg'); + $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpeg']); + $this->assertTrue($v->passes()); + + $file = $this->getMockBuilder(UploadedFile::class)->setMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock(); + $file->expects($this->any())->method('guessExtension')->willReturn('jpg'); + $file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpeg'); + $v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpg']); + $this->assertTrue($v->passes()); } public function testValidateMimeEnforcesPhpCheck() @@ -5674,6 +5719,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..8049c7aa5bc5 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); 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/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 52061422074e..ab003c3b9f9d 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); 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