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..f880d3b5ae62 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,10 @@ jobs: runs-on: ubuntu-latest services: + memcached: + image: memcached:1.6-alpine + ports: + - 11211:11211 mysql: image: mysql:5.7 env: @@ -40,13 +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" @@ -95,7 +96,7 @@ 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 diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index ae564658b926..42350000b97c 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,57 @@ # 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.13...6.x) + + +## [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..f1e61cd5215f 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,9 +1,181 @@ # 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.23.1...8.x) -## [v8.16.0 (2020-11-17)](https://github.com/laravel/framework/compare/v8.15.0...v8.16.0) +## [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..581d0ead203b 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" @@ -144,8 +144,8 @@ "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).", + "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/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..1e6f8c2b5165 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. * 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/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..ece6301c126f 100644 --- a/src/Illuminate/Bus/DatabaseBatchRepository.php +++ b/src/Illuminate/Bus/DatabaseBatchRepository.php @@ -4,11 +4,12 @@ use Carbon\CarbonImmutable; use Closure; +use DateTimeInterface; use Illuminate\Database\Connection; use Illuminate\Database\Query\Expression; use Illuminate\Support\Str; -class DatabaseBatchRepository implements BatchRepository +class DatabaseBatchRepository implements PrunableBatchRepository { /** * The batch factory instance. @@ -230,6 +231,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. * 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/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..eb035161ba87 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 */ 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..beaac46fe280 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -872,18 +872,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. * @@ -1128,7 +1116,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 +1168,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/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 2384948f951a..650213133ced 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -827,24 +827,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..0c0c7ce3c3ab 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -716,6 +716,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/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..765df0d873d8 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) 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/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 @@ +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..b4556d8fc305 --- /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..07630c590d5c 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -44,9 +44,11 @@ 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.', ]); } } diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index d3f1f96a8c13..aa579259c1d5 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. @@ -504,6 +508,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($this->model); + } + } + /** * Get a single column's value from the first result of a query. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 2a3ec0ee477b..0bde3e119227 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; } /** 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..36f3dad84a8a 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -105,12 +105,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 +481,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 +504,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 + )) )])]); } 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..03a47861643c 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. * @@ -563,6 +556,18 @@ public function orWherePivotNotNull($column) return $this->orWherePivotNull($column, true); } + /** + * 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 new instance of the related model. * @@ -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..ede942018f6f 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. * @@ -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..c9db5d2b2c6c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -326,7 +326,7 @@ public function morphWithCount(array $withCount) /** * Specify constraints on the query for a given morph types. * - * @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..2185ec641ac6 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -56,11 +56,11 @@ abstract class Relation 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. @@ -220,6 +220,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 +359,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/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/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 @@ +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/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/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/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/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index 91214b457188..dac103348a7d 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -432,7 +432,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 +487,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,9 +589,15 @@ 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; }); diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index f9d47250d08f..5575439418fc 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -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..4fdb7059931c 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.24.0'; /** * The base path for the Laravel installation. @@ -1311,8 +1311,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/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/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 924046b8b340..33fbccc5e7e1 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; @@ -89,6 +91,8 @@ class Handler implements ExceptionHandlerContract HttpException::class, HttpResponseException::class, ModelNotFoundException::class, + MultipleRecordsFoundException::class, + RecordsNotFoundException::class, SuspiciousOperationException::class, TokenMismatchException::class, ValidationException::class, @@ -369,6 +373,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/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/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/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/Wormhole.php b/src/Illuminate/Foundation/Testing/Wormhole.php index d660fe026a75..ef02e5a26f03 100644 --- a/src/Illuminate/Foundation/Testing/Wormhole.php +++ b/src/Illuminate/Foundation/Testing/Wormhole.php @@ -2,7 +2,7 @@ namespace Illuminate\Foundation\Testing; -use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Date; class Wormhole { @@ -32,7 +32,7 @@ public function __construct($value) */ public function milliseconds($callback = null) { - Carbon::setTestNow(Carbon::now()->addMilliseconds($this->value)); + Date::setTestNow(Date::now()->addMilliseconds($this->value)); return $this->handleCallback($callback); } @@ -45,7 +45,7 @@ public function milliseconds($callback = null) */ public function seconds($callback = null) { - Carbon::setTestNow(Carbon::now()->addSeconds($this->value)); + Date::setTestNow(Date::now()->addSeconds($this->value)); return $this->handleCallback($callback); } @@ -58,7 +58,7 @@ public function seconds($callback = null) */ public function minutes($callback = null) { - Carbon::setTestNow(Carbon::now()->addMinutes($this->value)); + Date::setTestNow(Date::now()->addMinutes($this->value)); return $this->handleCallback($callback); } @@ -71,7 +71,7 @@ public function minutes($callback = null) */ public function hours($callback = null) { - Carbon::setTestNow(Carbon::now()->addHours($this->value)); + Date::setTestNow(Date::now()->addHours($this->value)); return $this->handleCallback($callback); } @@ -84,7 +84,7 @@ public function hours($callback = null) */ public function days($callback = null) { - Carbon::setTestNow(Carbon::now()->addDays($this->value)); + Date::setTestNow(Date::now()->addDays($this->value)); return $this->handleCallback($callback); } @@ -97,7 +97,7 @@ public function days($callback = null) */ public function weeks($callback = null) { - Carbon::setTestNow(Carbon::now()->addWeeks($this->value)); + Date::setTestNow(Date::now()->addWeeks($this->value)); return $this->handleCallback($callback); } @@ -110,7 +110,7 @@ public function weeks($callback = null) */ public function years($callback = null) { - Carbon::setTestNow(Carbon::now()->addYears($this->value)); + Date::setTestNow(Date::now()->addYears($this->value)); return $this->handleCallback($callback); } @@ -122,9 +122,9 @@ public function years($callback = null) */ public static function back() { - Carbon::setTestNow(); + Date::setTestNow(); - return Carbon::now(); + return Date::now(); } /** @@ -137,7 +137,7 @@ protected function handleCallback($callback) { if ($callback) { return tap($callback(), function () { - Carbon::setTestNow(); + Date::setTestNow(); }); } } diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index d6af860d8915..3842915bf721 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -586,11 +586,15 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null) /** * Report an exception. * - * @param \Throwable $exception + * @param \Throwable|string $exception * @return void */ - function report(Throwable $exception) + function report($exception) { + if (is_string($exception)) { + $exception = new Exception($exception); + } + app(ExceptionHandler::class)->report($exception); } } @@ -601,7 +605,7 @@ function report(Throwable $exception) * * @param array|string|null $key * @param mixed $default - * @return \Illuminate\Http\Request|string|array + * @return \Illuminate\Http\Request|string|array|null */ function request($key = null, $default = null) { diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 7d8911605f51..7a21d96dca31 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 were 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..f65a8d5ca1ba 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -205,6 +205,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/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/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/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..a4567dadcb4e 100644 --- a/src/Illuminate/Mail/SendQueuedMailable.php +++ b/src/Illuminate/Mail/SendQueuedMailable.php @@ -2,11 +2,14 @@ namespace Illuminate\Mail; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Mail\Factory as MailFactory; use Illuminate\Contracts\Mail\Mailable as MailableContract; class SendQueuedMailable { + use Queueable; + /** * The mailable message instance. * @@ -39,6 +42,7 @@ 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; } /** 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..2f983023f051 100644 --- a/src/Illuminate/Notifications/SendQueuedNotifications.php +++ b/src/Illuminate/Notifications/SendQueuedNotifications.php @@ -64,6 +64,7 @@ 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; } /** 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(); 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/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/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/RedisConnectorTest.php b/tests/Redis/RedisConnectorTest.php index 36b488582215..ff5f93470e8d 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() diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 6715aa7297d9..9a9f76e8123f 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -1831,7 +1831,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/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 6faf65a1a3c2..2dfd503656ca 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -3590,6 +3590,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/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/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/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 8aae14243652..a231783eb804 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -2,8 +2,11 @@ namespace Illuminate\Tests\Foundation; +use Illuminate\Container\Container; use Illuminate\Contracts\View\View; +use Illuminate\Cookie\CookieValuePrefix; use Illuminate\Database\Eloquent\Model; +use Illuminate\Encryption\Encrypter; use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; use Illuminate\Testing\TestResponse; @@ -13,6 +16,7 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Cookie; class TestResponseTest extends TestCase { @@ -1154,6 +1158,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..3a97c0c962d1 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()); } @@ -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() diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 6872d38ae4a1..c9b89df6096e 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,69 @@ 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 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 +139,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 +158,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 +168,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 +204,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 +220,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 +236,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/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/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); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy