diff --git a/.gitattributes b/.gitattributes index b82e325ce02f..48a479d4b1be 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,11 @@ * text=auto +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + /.github export-ignore /bin export-ignore /tests export-ignore diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1f878f8a09da..00e38a2dce3f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - name: Feature request - url: https://github.com/laravel/framework/discussions/new - about: 'For ideas or feature requests' + url: https://github.com/laravel/ideas/issues + about: 'For ideas or feature requests, open up an issue on the Laravel ideas repository' - name: Support Questions & Other - url: https://github.com/laravel/framework/discussions/new - about: 'If you have a question or need help using the library' + url: https://laravel.com/docs/contributions#support-questions + about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:' - name: Documentation issue url: https://github.com/laravel/docs about: For documentation issues, open a pull request at the laravel/docs repository diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4c54650169cb..f880d3b5ae62 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,13 @@ on: jobs: linux_tests: - runs-on: ubuntu-latest + services: + memcached: + image: memcached:1.6-alpine + ports: + - 11211:11211 mysql: image: mysql:5.7 env: @@ -27,7 +31,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.3, 7.4] + php: [7.3, 7.4, 8.0] stability: [prefer-lowest, prefer-stable] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} @@ -40,15 +44,27 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached tools: composer:v2 coverage: none - - name: Setup Memcached - uses: niden/actions-memcached@v7 + - name: Setup problem matchers + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Set Minimum Guzzle Version + uses: nick-invision/retry@v1 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer require guzzlehttp/guzzle:^7.2 --no-interaction --no-update + if: matrix.php >= 8 - name: Install dependencies - run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + uses: nick-invision/retry@v1 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress - name: Execute tests run: vendor/bin/phpunit --verbose @@ -57,17 +73,13 @@ jobs: DB_USERNAME: root windows_tests: - runs-on: windows-latest + strategy: fail-fast: true matrix: - php: [7.3, 7.4] - include: - - php: 7.3 - stability: prefer-lowest - - php: 7.4 - stability: prefer-stable + php: [7.3, 7.4, 8.0] + stability: [prefer-lowest, prefer-stable] name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - Windows @@ -84,13 +96,27 @@ 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 - ini-values: memory_limit=512M + + - name: Setup problem matchers + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Set Minimum Guzzle Version + uses: nick-invision/retry@v1 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer require guzzlehttp/guzzle:^7.2 --no-interaction --no-update + if: matrix.php >= 8 - name: Install dependencies - run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + uses: nick-invision/retry@v1 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress - name: Execute tests run: vendor/bin/phpunit --verbose diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index 09aae92bdff2..3ab00165cccc 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,120 @@ # Release Notes for 6.x -## [Unreleased](https://github.com/laravel/framework/compare/v6.18.40...6.x) +## [Unreleased](https://github.com/laravel/framework/compare/v6.20.10...6.x) + + +## [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) + +### Fixed +- Fixing BroadcastException message in PusherBroadcaster@broadcast ([#35290](https://github.com/laravel/framework/pull/35290)) +- Fixed generic DetectsLostConnection string ([#35323](https://github.com/laravel/framework/pull/35323)) + +### Changed +- Updated `aws/aws-sdk-php` suggest to `^3.155` ([#35267](https://github.com/laravel/framework/pull/35267)) + + +## [v6.20.4 (2020-11-17)](https://github.com/laravel/framework/compare/v6.20.3...v6.20.4) + +### Fixed +- Fixed pivot restoration ([#35218](https://github.com/laravel/framework/pull/35218)) + + +## [v6.20.3 (2020-11-10)](https://github.com/laravel/framework/compare/v6.20.2...v6.20.3) + +### Fixed +- Turn the eloquent collection into a base collection if mapWithKeys loses models ([#35129](https://github.com/laravel/framework/pull/35129)) + + +## [v6.20.2 (2020-10-29)](https://github.com/laravel/framework/compare/v6.20.1...v6.20.2) + +### Fixed +- [Add some fixes](https://github.com/laravel/framework/compare/v6.20.1...v6.20.2) + + +## [v6.20.1 (2020-10-29)](https://github.com/laravel/framework/compare/v6.20.0...v6.20.1) + +### Fixed +- Fixed alias usage in `Eloquent` ([6091048](https://github.com/laravel/framework/commit/609104806b8b639710268c75c22f43034c2b72db)) +- Fixed `Illuminate\Support\Reflector::isCallable()` ([a90f344](https://github.com/laravel/framework/commit/a90f344c66f0a5bb1d718f8bbd20c257d4de9e02)) + + +## [v6.20.0 (2020-10-28)](https://github.com/laravel/framework/compare/v6.19.1...v6.20.0) + +### Added +- Full PHP 8.0 Support ([#33388](https://github.com/laravel/framework/pull/33388)) +- Added `Illuminate\Support\Reflector::isCallable()` ([#34994](https://github.com/laravel/framework/pull/34994), [8c16891](https://github.com/laravel/framework/commit/8c16891c6e7a4738d63788f4447614056ab5136e), [31917ab](https://github.com/laravel/framework/commit/31917abcfa0db6ec6221bb07fc91b6e768ff5ec8), [11cfa4d](https://github.com/laravel/framework/commit/11cfa4d4c92bf2f023544d58d51b35c5d31dece0), [#34999](https://github.com/laravel/framework/pull/34999)) + +### Changed +- Bump minimum PHP version to v7.2.5 ([#34928](https://github.com/laravel/framework/pull/34928)) + +### Fixed +- Fixed ambigious column on many to many with select load ([5007986](https://github.com/laravel/framework/commit/500798623d100a9746b2931ae6191cb756521f05)) + + +## [v6.19.1 (2020-10-20)](https://github.com/laravel/framework/compare/v6.19.0...v6.19.1) + +### Fixed +- Fixed `bound()` method ([a7759d7](https://github.com/laravel/framework/commit/a7759d70e15b0be946569b8299ac694c08a35d7e)) + + +## [v6.19.0 (2020-10-20)](https://github.com/laravel/framework/compare/v6.18.43...v6.19.0) + +### Added +- Provisional support for PHP 8.0 ([#34884](https://github.com/laravel/framework/pull/34884), [28bb76e](https://github.com/laravel/framework/commit/28bb76efbcfc5fee57307ffa062b67ff709240dc)) + + +## [v6.18.43 (2020-10-13)](https://github.com/laravel/framework/compare/v6.18.42...v6.18.43) + +### Fixed +- Matched `symfony/debug` version with other symfony reqs ([6ce02a2](https://github.com/laravel/framework/commit/6ce02a21cf736f28beda2529d1e28849e86b0944)) + + +## [v6.18.42 (2020-10-06)](https://github.com/laravel/framework/compare/v6.18.41...v6.18.42) + +### Fixed +- Added missed RESET_THROTTLED constant to Password Facade ([#34641](https://github.com/laravel/framework/pull/34641)) + + +## [v6.18.41 (2020-09-29)](https://github.com/laravel/framework/compare/v6.18.40...v6.18.41) + +### Fixed +- Added support for stream reads in FileManager for S3 driver ([#34480](https://github.com/laravel/framework/pull/34480)) ## [v6.18.40 (2020-09-09)](https://github.com/laravel/framework/compare/v6.18.39...v6.18.40) @@ -49,7 +163,7 @@ ### Fixed - Fixed `Illuminate\Support\Arr::query()` ([c6f9ae2](https://github.com/laravel/framework/commit/c6f9ae2b6fdc3c1716938223de731b97f6a5a255)) -- Dont allow mass filling with table names ([9240404](https://github.com/laravel/framework/commit/9240404b22ef6f9e827577b3753e4713ddce7471), [f5fa6e3](https://github.com/laravel/framework/commit/f5fa6e3a0fbf9a93eab45b9ae73265b4dbfc3ad7)) +- Don't allow mass filling with table names ([9240404](https://github.com/laravel/framework/commit/9240404b22ef6f9e827577b3753e4713ddce7471), [f5fa6e3](https://github.com/laravel/framework/commit/f5fa6e3a0fbf9a93eab45b9ae73265b4dbfc3ad7)) ## [v6.18.33 (2020-08-06)](https://github.com/laravel/framework/compare/v6.18.32...v6.18.33) @@ -93,7 +207,7 @@ ## [v6.18.27 (2020-07-27)](https://github.com/laravel/framework/compare/v6.18.26...v6.18.27) ### Fixed -- Dont decrement transaction below 0 in `Illuminate\Database\Concerns\ManagesTransactions::handleCommitTransactionException()` ([7681795](https://github.com/laravel/framework/commit/768179578e5492b5f80c391bd43b233938e16e27)) +- Don't decrement transaction below 0 in `Illuminate\Database\Concerns\ManagesTransactions::handleCommitTransactionException()` ([7681795](https://github.com/laravel/framework/commit/768179578e5492b5f80c391bd43b233938e16e27)) - Fixed transaction problems on closure transaction ([c4cdfc7](https://github.com/laravel/framework/commit/c4cdfc7c54127b772ef10f37cfc9ef8e9d6b3227)) - Prevent to serialize uninitialized properties ([#33644](https://github.com/laravel/framework/pull/33644)) - Fixed missing statement preventing deletion in `Illuminate\Database\Eloquent\Relations\MorphPivot::delete()` ([#33648](https://github.com/laravel/framework/pull/33648)) @@ -442,7 +556,7 @@ ### Changed - Use SKIP LOCKED for mysql 8.1 and pgsql 9.5 queue workers ([#31287](https://github.com/laravel/framework/pull/31287)) -- Dont merge middleware from method and property in `Illuminate\Bus\Queueable::middleware()` ([#31301](https://github.com/laravel/framework/pull/31301)) +- Don't merge middleware from method and property in `Illuminate\Bus\Queueable::middleware()` ([#31301](https://github.com/laravel/framework/pull/31301)) - Split `specifyParameter()` from `Illuminate\Console\Command` to `HasParameters` trait ([#31254](https://github.com/laravel/framework/pull/31254)) - Make sure changing a database field to json does not include charset ([#31343](https://github.com/laravel/framework/pull/31343)) @@ -581,7 +695,7 @@ - Fixed `Builder::withCount()` binding error when a scope is added into related model with binding in a sub-select ([#30869](https://github.com/laravel/framework/pull/30869)) ### Changed -- Dont throw exception when session is not set in `AuthenticateSession` middleware ([4de1d24](https://github.com/laravel/framework/commit/4de1d24cf390f07d4f503973e5556f73060fbb31)) +- Don't throw exception when session is not set in `AuthenticateSession` middleware ([4de1d24](https://github.com/laravel/framework/commit/4de1d24cf390f07d4f503973e5556f73060fbb31)) ## [v6.8.0 (2019-12-17)](https://github.com/laravel/framework/compare/v6.7.0...v6.8.0) diff --git a/CHANGELOG-7.x.md b/CHANGELOG-7.x.md deleted file mode 100644 index 9652d7ec3d04..000000000000 --- a/CHANGELOG-7.x.md +++ /dev/null @@ -1,893 +0,0 @@ -# Release Notes for 7.x - -## [Unreleased](https://github.com/laravel/framework/compare/v7.28.2...7.x) - - -## [v7.28.2 (2020-09-15)](https://github.com/laravel/framework/compare/v7.28.1...v7.28.2) - -### Fixed -- Do not used `now` helper in `Illuminate/Cache/DatabaseLock::expiresAt()` ([#34262](https://github.com/laravel/framework/pull/34262)) -- Fixed `Illuminate\View\ComponentAttributeBag::whereDoesntStartWith()` ([#34329](https://github.com/laravel/framework/pull/34329)) - - -## [v7.28.1 (2020-09-09)](https://github.com/laravel/framework/compare/v7.28.0...v7.28.1) - -### Revert -- Revert of ["Fixed for empty fallback_locale in `Illuminate\Translation\Translator`"](https://github.com/laravel/framework/pull/34136) ([7c54eb6](https://github.com/laravel/framework/commit/7c54eb678d58fb9ee7f532a5a5842e6f0e1fe4c9)) - - -## [v7.28.0 (2020-09-08)](https://github.com/laravel/framework/compare/v7.27.0...v7.28.0) - -### Added -- Added expectsTable console assertion ([74e1fca](https://github.com/laravel/framework/commit/74e1fca5fa333e32e24a7aa24049d5303a1bf281), [c6cf381](https://github.com/laravel/framework/commit/c6cf38139d2524a7c3accb606e3fb1b035c98d6a)) - -### Fixed -- Use `getTouchedRelations` when touching owners ([#34100](https://github.com/laravel/framework/pull/34100)) -- Fixed for empty fallback_locale in `Illuminate\Translation\Translator` ([#34136](https://github.com/laravel/framework/pull/34136)) -- Fixed `Illuminate\Database\Schema\Grammars\SqlServerGrammar::compileColumnListing()` for tables with schema ([#34076](https://github.com/laravel/framework/pull/34076)) -- Fixed Significant performance issue in Eloquent Collection loadCount() method ([#34177](https://github.com/laravel/framework/pull/34177)) - - -## [v7.27.0 (2020-09-01)](https://github.com/laravel/framework/compare/v7.26.1...v7.27.0) - -### Added -- Allow to use alias of morphed model ([#34032](https://github.com/laravel/framework/pull/34032)) -- Introduced basic padding (both, left, right) methods to Str and Stringable ([#34053](https://github.com/laravel/framework/pull/34053)) - -### Refactoring -- RefreshDatabase migration commands parameters moved to methods ([#34007](https://github.com/laravel/framework/pull/34007), [8b35c8e](https://github.com/laravel/framework/commit/8b35c8e6ba5879e71fd81fd03b5687ee2b46c55a), [256f71c](https://github.com/laravel/framework/commit/256f71c1f81da2d4bb3e327b18389ac43fa97a72)) - -### Changed -- allow to reset forced scheme and root-url in UrlGenerator ([#34039](https://github.com/laravel/framework/pull/34039)) -- Updating the make commands to use a custom views path ([#34060](https://github.com/laravel/framework/pull/34060), [b593c62](https://github.com/laravel/framework/commit/b593c6242942623fcc12638d0390da7c58dbbb11)) -- Using "public static property" in View Component causes an error ([#34058](https://github.com/laravel/framework/pull/34058)) -- Changed postgres processor ([#34055](https://github.com/laravel/framework/pull/34055)) - - -## [v7.26.1 (2020-08-27)](https://github.com/laravel/framework/compare/v7.26.0...v7.26.1) - -### Fixed -- Fixed offset error on invalid remember token ([#34020](https://github.com/laravel/framework/pull/34020)) -- Only prepend scheme to PhpRedis host when necessary ([#34017](https://github.com/laravel/framework/pull/34017)) -- Fixed `whereKey` and `whereKeyNot` in `Illuminate\Database\Eloquent\Builder` ([#34031](https://github.com/laravel/framework/pull/34031)) - - -## [v7.26.0 (2020-08-25)](https://github.com/laravel/framework/compare/v7.25.0...v7.26.0) - -### Added -- Added `whenHas` and `whenFilled` methods to `Illuminate\Http\Concerns\InteractsWithInput` class ([#33829](https://github.com/laravel/framework/pull/33829)) -- Added email validating with custom class ([#33835](https://github.com/laravel/framework/pull/33835)) -- Added `Illuminate\View\ComponentAttributeBag::whereDoesntStartWith()` ([#33851](https://github.com/laravel/framework/pull/33851)) -- Allow setting synchronous_commit for Postgres ([#33897](https://github.com/laravel/framework/pull/33897)) -- Allow nested errors in `Illuminate\Testing\TestResponse::assertJsonValidationErrors()` ([#33989](https://github.com/laravel/framework/pull/33989)) -- Added support for stream reads to `FilesystemManager` ([#34001](https://github.com/laravel/framework/pull/34001)) - -### Fixed -- Fix defaultTimezone not respected in scheduled Events ([#33834](https://github.com/laravel/framework/pull/33834)) -- Fixed usage of Support `Collection#countBy($key)` ([#33852](https://github.com/laravel/framework/pull/33852)) -- Fixed route registerar bug ([42ba0ef](https://github.com/laravel/framework/commit/42ba0ef3e379cb1e0fa38c3d3297109ff1234a1d)) -- Fixed key composition for attribute with dot at validation error messages ([#33932](https://github.com/laravel/framework/pull/33932)) -- Fixed the `dump` method for `LazyCollection` ([#33944](https://github.com/laravel/framework/pull/33944)) -- Fixed dimension ratio calculation in `Illuminate\Validation\Concerns\ValidatesAttributes::failsRatioCheck()` ([#34003](https://github.com/laravel/framework/pull/34003)) - -### Changed -- Implement LockProvider on DatabaseStore ([#33844](https://github.com/laravel/framework/pull/33844)) -- Publish resources.stub in stub:publish command ([#33862](https://github.com/laravel/framework/pull/33862)) -- Handle argon failures robustly ([#33856](https://github.com/laravel/framework/pull/33856)) -- Normalize scheme in Redis connections ([#33892](https://github.com/laravel/framework/pull/33892)) -- Cast primary key to string when $keyType is string ([#33930](https://github.com/laravel/framework/pull/33930)) -- Load anonymous components from packages ([#33954](https://github.com/laravel/framework/pull/33954)) -- Check no-interaction flag exists and is true for Artisan commands ([#33950](https://github.com/laravel/framework/pull/33950)) - -### Deprecated -- Deprecate `Illuminate\Database\Eloquent\Model::removeTableFromKey()` ([#33859](https://github.com/laravel/framework/pull/33859)) - - -## [v7.25.0 (2020-08-11)](https://github.com/laravel/framework/compare/v7.24.0...v7.25.0) - -### Added -- Added support to use `where` in `apiResource` method ([#33790](https://github.com/laravel/framework/pull/33790), [3dcc4a6](https://github.com/laravel/framework/commit/3dcc4a6bc6640b3d577c6740d63b6ef3df42e124)) -- Support `tls://` scheme when using `url` in Redis config ([#33800](https://github.com/laravel/framework/pull/33800)) -- Scoped resource routes ([#33752](https://github.com/laravel/framework/pull/33752)) -- Added Once blade Blocks ([#33812](https://github.com/laravel/framework/pull/33812)) -- Let mailables accept a simple array of email addresses as cc or bcc ([#33810](https://github.com/laravel/framework/pull/33810)) -- Added support for PhpRedis 5.3 options parameter ([#33799](https://github.com/laravel/framework/pull/33799)) - -### Changed -- Removed quotes when setting isolation level for mysql connections ([#33805](https://github.com/laravel/framework/pull/33805)) -- Make LazyCollection#countBy be lazy ([#33801](https://github.com/laravel/framework/pull/33801)) - -### Fixed -- Revert changes to MailMessage ([#33816](https://github.com/laravel/framework/pull/33816)) - - -## [v7.24.0 (2020-08-07)](https://github.com/laravel/framework/compare/v7.23.2...v7.24.0) - -### Added -- Added possibility to configure isolation level for mysql connections ([#33783](https://github.com/laravel/framework/pull/33783), [c6a3174](https://github.com/laravel/framework/commit/c6a317405e5e9075206a019246a8a79d0c68def4)) -- Added plain text only notifications ([#33781](https://github.com/laravel/framework/pull/33781)) - -### Changed -- Verify column names are actual columns when using guarded ([#33777](https://github.com/laravel/framework/pull/33777)) - - -## [v7.23.2 (2020-08-06)](https://github.com/laravel/framework/compare/v7.23.1...v7.23.2) - -### Fixed -- Fixed `Illuminate\Support\Arr::query()` ([c6f9ae2](https://github.com/laravel/framework/commit/c6f9ae2b6fdc3c1716938223de731b97f6a5a255)) -- Dont allow mass filling with table names ([9240404](https://github.com/laravel/framework/commit/9240404b22ef6f9e827577b3753e4713ddce7471), [f5fa6e3](https://github.com/laravel/framework/commit/f5fa6e3a0fbf9a93eab45b9ae73265b4dbfc3ad7)) - - -## [v7.23.1 (2020-08-06)](https://github.com/laravel/framework/compare/v7.23.0...v7.23.1) - -### Added -- Added isNotFilled() method to Request ([#33732](https://github.com/laravel/framework/pull/33732)) - -### Fixed -- Fixed `Illuminate\Database\Eloquent\Concerns\GuardsAttributes::isGuarded()` ([1b70bef](https://github.com/laravel/framework/commit/1b70bef5fd7cc5da74abcdf79e283f830fa3b0a4), [624d873](https://github.com/laravel/framework/commit/624d873733388aa2246553a3b465e38554953180), [b70876a](https://github.com/laravel/framework/commit/b70876ac80759fbf168c91cdffd7a2b2305e27cb)) -- Fixed escaping quotes ([687df01](https://github.com/laravel/framework/commit/687df01fa19c99546c1ae1dd53c2a465459b50dc)) - - -## [v7.23.0 (2020-08-04)](https://github.com/laravel/framework/compare/v7.22.4...v7.23.0) - -### Added -- Added dynamic slot (directive) name support ([#33724](https://github.com/laravel/framework/pull/33724)) -- Added plain mail to notifications ([#33725](https://github.com/laravel/framework/pull/33725)) -- Support the `sink` option when using Http::fake() ([#33720](https://github.com/laravel/framework/pull/33720), [fba984b](https://github.com/laravel/framework/commit/fba984b05081f8aee19447caa0d92624bcf04312)) -- Added whereBetweenColumns | orWhereBetweenColumns | whereNotBetweenColumns | orWhereNotBetweenColumns methods to `Illuminate\Database\Query\Builder` ([#33728](https://github.com/laravel/framework/pull/33728)) - -### Changed -- Ignore numeric field names in validators ([#33712](https://github.com/laravel/framework/pull/33712)) -- Fixed validation rule 'required_unless' when other field value is boolean. ([#33715](https://github.com/laravel/framework/pull/33715)) - - -## [v7.22.4 (2020-07-27)](https://github.com/laravel/framework/compare/v7.22.3...v7.22.4) - -### Update -- Update cookies encryption ([release](https://github.com/laravel/framework/compare/v7.22.3...v7.22.4)) - - -## [v7.22.3 (2020-07-27)](https://github.com/laravel/framework/compare/v7.22.2...v7.22.3) - -### Update -- Update cookies encryption ([release](https://github.com/laravel/framework/compare/v7.22.2...v7.22.3)) - - -## [v7.22.2 (2020-07-27)](https://github.com/laravel/framework/compare/v7.22.1...v7.22.2) - -### Fixed -- Fixed cookie issues encryption ([c9ce261](https://github.com/laravel/framework/commit/c9ce261a9f7b8e07c9ebc8a7d45651ee1cf86215), [5786aa4](https://github.com/laravel/framework/commit/5786aa4a388adfcc62862573275bd37d49aa07d7)) - - -## [v7.22.1 (2020-07-27)](https://github.com/laravel/framework/compare/v7.22.0...v7.22.1) - -### Fixed -- Fixed cookie issues ([bb9db21](https://github.com/laravel/framework/commit/bb9db21af137344feffa192fcabe4e439c8b0f60)) - - -## [v7.22.0 (2020-07-27)](https://github.com/laravel/framework/compare/v7.21.0...v7.22.0) - -### Added -- Added `sectionMissing` Blade Directive ([#33614](https://github.com/laravel/framework/pull/33614)) -- Added range option to queue:retry command ([#33627](https://github.com/laravel/framework/pull/33627)) - -### Fixed -- Prevent usage of get*AtColumn() when model has no timestamps ([#33634](https://github.com/laravel/framework/pull/33634)) -- Dont decrement transaction below 0 in `Illuminate\Database\Concerns\ManagesTransactions::handleCommitTransactionException()` ([7681795](https://github.com/laravel/framework/commit/768179578e5492b5f80c391bd43b233938e16e27)) -- Fixed transaction problems on closure transaction ([c4cdfc7](https://github.com/laravel/framework/commit/c4cdfc7c54127b772ef10f37cfc9ef8e9d6b3227)) -- Prevent to serialize uninitialized properties ([#33644](https://github.com/laravel/framework/pull/33644)) -- Fixed missing statement preventing deletion in `Illuminate\Database\Eloquent\Relations\MorphPivot::delete()` ([#33648](https://github.com/laravel/framework/pull/33648)) - -### Changed -- Throw a TypeError if concrete is not a string or closure in `Illuminate\Container\Container::bind()` ([#33539](https://github.com/laravel/framework/pull/33539)) -- Add HTML comment block around inline inspiring quote for consistency with blade template version ([#33625](https://github.com/laravel/framework/pull/33625)) -- Improve cookie encryption ([#33662](https://github.com/laravel/framework/pull/33662)) - - -## [v7.21.0 (2020-07-21)](https://github.com/laravel/framework/compare/v7.20.0...v7.21.0) - -### Added -- Added `Illuminate\Database\Schema\ForeignKeyDefinition::nullOnDelete()` ([#33551](https://github.com/laravel/framework/pull/33551)) -- Added `getFallbackLocale()` and `setFallbackLocale()` methods to `Illuminate\Foundation\Application` ([#33595](https://github.com/laravel/framework/pull/33595)) - -### Fixed -- Fixed `Illuminate/Redis/Connections/PhpRedisConnection::*scan()` returns ([d3d36f0](https://github.com/laravel/framework/commit/d3d36f059ef1c56e17d8e434e9fd3dfd6cbe6e53)) -- Align (fix) nested arrays support for `assertViewHas` & `assertViewMissing` in `Illuminate\Testing\TestResponse` ([#33566](https://github.com/laravel/framework/pull/33566)) -- Fixed issue where Storage::path breaks when using cache due to missing method in CachedAdapter ([#33602](https://github.com/laravel/framework/pull/33602)) - -### Changed -- Added a base exception for Http Client exceptions ([#33581](https://github.com/laravel/framework/pull/33581)) - - -## [v7.20.0 (2020-07-14)](https://github.com/laravel/framework/compare/v7.19.1...v7.20.0) - -### Added -- Added `Illuminate\Database\Schema\ForeignKeyDefinition::cascadeOnUpdate()` ([#33522](https://github.com/laravel/framework/pull/33522)) - -### Changed -- Apply model connection name to Database validation rules ([#33525](https://github.com/laravel/framework/pull/33525)) -- Allow calling invokable classes using FQN in `Illuminate\Container\BoundMethod.php::call()` ([#33535](https://github.com/laravel/framework/pull/33535)) - - -## [v7.19.1 (2020-07-10)](https://github.com/laravel/framework/compare/v7.19.0...v7.19.1) - -### Added -- Added support for SQL Server LoginTimeout to specify seconds to wait before failing connection attempt ([#33472](https://github.com/laravel/framework/pull/33472)) -- Added ability to simulate "withCredentials" in test requests ([#33497](https://github.com/laravel/framework/pull/33497), [aa17e75](https://github.com/laravel/framework/commit/aa17e75f216c58f03652625866f5ac5c2fcbcab7)) - -### Fixed -- Fixed `Illuminate\Cache\FileStore::flush()` ([#33458](https://github.com/laravel/framework/pull/33458)) -- Fixed auto creating model by class name ([#33481](https://github.com/laravel/framework/pull/33481)) -- Don't return nested data from validator when failing an exclude rule ([#33435](https://github.com/laravel/framework/pull/33435)) -- Fixed validation nested error messages ([6615371](https://github.com/laravel/framework/commit/6615371d7c0a7431372244d21eae54696b3c19f2)) -- Fixed `Illuminate\Support\Reflector` to handle parent ([#33502](https://github.com/laravel/framework/pull/33502)) - -### Revert -- Revert [Improve SQL Server last insert id retrieval](https://github.com/laravel/framework/pull/33453) ([#33496](https://github.com/laravel/framework/pull/33496)) - - -## [v7.19.0 (2020-07-07)](https://github.com/laravel/framework/compare/v7.18.0...v7.19.0) - -### Added -- Added `everyTwoHours()` | `everyThreeHours()` | `everyFourHours()` | `everySixHours()` methods to `Illuminate\Console\Scheduling\ManagesFrequencies` ([#33393](https://github.com/laravel/framework/pull/33393)) -- Conditionally returning appended attributes in API resources ([#33422](https://github.com/laravel/framework/pull/33422)) -- Added `ScheduledTaskFailed` event ([#33427](https://github.com/laravel/framework/pull/33427)) -- Added `Illuminate\Support\Stringable::when()` ([#33455](https://github.com/laravel/framework/pull/33455)) - -### Fixed -- Fixed signed urls with custom parameters ([bcb133e](https://github.com/laravel/framework/commit/bcb133e46906e748067772cf49b2f355441815c5)) -- Determine model key name correctly in Illuminate/Validation/Concerns/ValidatesAttributes.php ([a1fdd53](https://github.com/laravel/framework/commit/a1fdd536c542dabbe9882f50e849cc177dc0ad88)) -- Fixed notifications database channel for anonymous notifiables ([#33409](https://github.com/laravel/framework/pull/33409)) - -### Changed -- Improve SQL Server last insert id retrieval ([#33430](https://github.com/laravel/framework/pull/33430), [de1d159](https://github.com/laravel/framework/commit/de1d1592f3a69bd9952431ee67e76996d00e001c)) -- Make Str::endsWith return false if both haystack and needle are empty strings ([#33434](https://github.com/laravel/framework/pull/33434)) - - -## [v7.18.0 (2020-06-30)](https://github.com/laravel/framework/compare/v7.17.2...v7.18.0) - -### Added -- Added `Illuminate\Http\Client\PendingRequest::withMiddleware()` ([#33315](https://github.com/laravel/framework/pull/33315), [b718d3a](https://github.com/laravel/framework/commit/b718d3a06d7009c0fd0237222602c1e42681b6a3)) -- Make ComponentAttributeBag Macroable ([#33354](https://github.com/laravel/framework/pull/33354)) -- Added `filter` and `whereStartsWith` and `thatStartWith` to `Illuminate\View\ComponentAttributeBag` ([0abe2db](https://github.com/laravel/framework/commit/0abe2dbed9d9b1c4a733a4c24e8383d747134286), [07ee3e8](https://github.com/laravel/framework/commit/07ee3e820b34df5e422fb868886fd190880dfc7f)) -- Added `Illuminate\Database\Eloquent\Collection::toQuery()` ([#33356](https://github.com/laravel/framework/pull/33356), [15586fa](https://github.com/laravel/framework/commit/15586fa6691884db18627721f6e143c3e035ddc0)) -- Added `first()` to `Illuminate\View\ComponentAttributeBag` ([#33358](https://github.com/laravel/framework/pull/33358), [731b94f](https://github.com/laravel/framework/commit/731b94f1734dcdb97a9466948111ab639ac11a2a)) -- Added `everyTwoMinutes()` | `everyThreeMinutes()` | `everyFourMinutes()` methods to `Illuminate/Console/Scheduling/ManagesFrequencies` ([#33379](https://github.com/laravel/framework/pull/33379)) - -### Fixed -- Fixed `ConfigurationUrlParser` query decoding ([#33340](https://github.com/laravel/framework/pull/33340)) -- Fixed exists in `Illuminate\Database\Eloquent\Relations\Concerns\AsPivot::delete()` ([#33347](https://github.com/laravel/framework/pull/33347)) - -### Changed -- Replace placeholder for dots and asterisks in validator ([#33367](https://github.com/laravel/framework/pull/33367)) - - -## [v7.17.2 (2020-06-24)](https://github.com/laravel/framework/compare/v7.17.1...v7.17.2) - -### Added -- Added `Illuminate\Http\Client\PendingRequest::withBody()` method ([1e1f531](https://github.com/laravel/framework/commit/1e1f5311f062d62468fe2d3cef1695b8fa338cfb), [7b0b437](https://github.com/laravel/framework/commit/7b0b4375bbe231a3b96c739ff144b1df1465a387)) - -### Fixed -- Fixed `Illuminate\Database\Eloquent\Concerns\HasAttributes::getOriginal()` ([b20125d](https://github.com/laravel/framework/commit/b20125d7bf270bcd6cc651114512a2dc7f182a96), [899c765](https://github.com/laravel/framework/commit/899c765e89573d8a64e16b008af519096e12d534), [2937cce](https://github.com/laravel/framework/commit/2937cce360f4feb96e93d6cf86e24f2e8c0832fc)) - -### Revert -- Revert "Fixed `Model::originalIsEquivalent()` with floats ([#33259](https://github.com/laravel/framework/pull/33259), [d68d915](https://github.com/laravel/framework/commit/d68d91516db6d1b9cba8a72f99b2c7e8223e988f))" [bf3cb6f](https://github.com/laravel/framework/commit/bf3cb6f6979df2d6965d2e0aa731724d0e2b15e5) - - -## [v7.17.1 (2020-06-23)](https://github.com/laravel/framework/compare/v7.17.0...v7.17.1) - -### Fixed -- Fixed "Undefined variable: current" exception in `Illuminate\Database\Eloquent\Concerns\HasAttributes::originalIsEquivalent()` [#33308](https://github.com/laravel/framework/pull/33308) - - -## [v7.17.0 (2020-06-23)](https://github.com/laravel/framework/compare/v7.16.1...v7.17.0) - -### Added -- Added `Illuminate\Console\Scheduling\ManagesFrequencies::lastDayOfMonth()` ([#33241](https://github.com/laravel/framework/pull/33241), [be194a8](https://github.com/laravel/framework/commit/be194a8a7b302fa68b1b2ed66d440f9f91dfec9f)) -- Allow array based event listeners ([7594267](https://github.com/laravel/framework/commit/75942673f6f54dc70fec246051171183af8e06e3)) -- Allow array callback format with non-static methods in `Illuminate\Auth\Access\Gate::define()` ([b7977d3](https://github.com/laravel/framework/commit/b7977d322a2c9baf28cc127cee09c70727c5f56e)) -- Added `Illuminate\Console\Scheduling\ManagesFrequencies::time()` parameter on twiceMonthly function ([#33274](https://github.com/laravel/framework/pull/33274)) -- Added `providerIsLoaded` method to `Illuminate\Foundation\Application` ([#33286](https://github.com/laravel/framework/pull/33286), [b87233f](https://github.com/laravel/framework/commit/b87233f48da0b4f219adebd851acd22058dfd551)) - -### Fixed -- Fixed domain binding with custom fields in `Illuminate\Routing\Route::domain()` ([#33231](https://github.com/laravel/framework/pull/33231)) -- Fixed `Model::originalIsEquivalent()` with floats ([#33259](https://github.com/laravel/framework/pull/33259), [d68d915](https://github.com/laravel/framework/commit/d68d91516db6d1b9cba8a72f99b2c7e8223e988f)) - - -## [v7.16.1 (2020-06-16)](https://github.com/laravel/framework/compare/v7.16.0...v7.16.1) - -### Revert -- Revert "handle array callbacks" in event dispatcher ([4e3fedb](https://github.com/laravel/framework/commit/4e3fedb2a401986676f9d6aa5f244e95e9c92444)) - - -## [v7.16.0 (2020-06-16)](https://github.com/laravel/framework/compare/v7.15.0...v7.16.0) - -### Added -- Added `makeVisibleIf` and `makeHiddenIf` methods to `Illuminate\Database\Eloquent\Concerns\HidesAttributes` ([#33176](https://github.com/laravel/framework/pull/33176), [42383e4](https://github.com/laravel/framework/commit/42383e4ba8806ac0ab69f80d0325fa01fd9c30f4)) -- Added option to specify a custom guard for the `make:policy` command ([#33210](https://github.com/laravel/framework/pull/33210), [13e3b65](https://github.com/laravel/framework/commit/13e3b65bad5062eeba34aa2f39effd0fc4081ccd)) -- Added `theme` property to `Illuminate\Mail\Mailable` class ([#33218](https://github.com/laravel/framework/pull/33218)) - -### Changed -- Improved the reflector ([#33184](https://github.com/laravel/framework/pull/33184)) -- Streamline ease of use with relation subquery ([#33180](https://github.com/laravel/framework/pull/33180)) -- Improve event subscribers ([#33191](https://github.com/laravel/framework/pull/33191), [058d92f](https://github.com/laravel/framework/commit/058d92f2842211a0bc60222fd464ca5350965c22), [b80ddf4](https://github.com/laravel/framework/commit/b80ddf458bd08de375d83b716a1309ed927197aa)) - - -## [v7.15.0 (2020-06-09)](https://github.com/laravel/framework/compare/v7.14.1...v7.15.0) - -### Added -- Added extendable relations for models ([#33025](https://github.com/laravel/framework/pull/33025)) -- Added `Illuminate\Foundation\Testing\Concerns\MakesHttpRequests::withToken()` ([#33075](https://github.com/laravel/framework/pull/33075), [79383a1](https://github.com/laravel/framework/commit/79383a129bf213177ff00ec1ba7c396da5d7749b)) -- Added the ability to `Illuminate\Database\Eloquent\Relations\HasOneOrMany::makeMany()` (create many without saving) ([#33021](https://github.com/laravel/framework/pull/33021)) -- Added `Illuminate\Database\Schema\Blueprint::foreignUuid()` ([#33129](https://github.com/laravel/framework/pull/33129)) -- Allow setting the event handler queue via a `viaQueue()` method ([#32770](https://github.com/laravel/framework/pull/32770), [852a927](https://github.com/laravel/framework/commit/852a927d254af9719c9fde6eb31466472fd03dfc)) - -### Fixed -- Fixed `Model::withoutEvents()` not registering listeners inside boot() ([#33149](https://github.com/laravel/framework/pull/33149), [4bb32ae](https://github.com/laravel/framework/commit/4bb32aea50eec4c3cc8b77f463e4a96213a0af09)) - - -## [v7.14.1 (2020-06-03)](https://github.com/laravel/framework/compare/v7.14.0...v7.14.1) - -### Added -- Added missing `symfony/mime` suggest ([#33067](https://github.com/laravel/framework/pull/33067)) - -### Fixed -- Fixed `Illuminate\Database\Eloquent\Relations\MorphToMany::getCurrentlyAttachedPivots()` ([110b129](https://github.com/laravel/framework/commit/110b129531df172f03bf163f561c71123fac6296)) - - -## [v7.14.0 (2020-06-02)](https://github.com/laravel/framework/compare/v7.13.0...v7.14.0) - -### Added -- Views: make attributes available within render method ([#32978](https://github.com/laravel/framework/pull/32978)) -- Added `forceDeleted` method to `SoftDeletes` ([#32982](https://github.com/laravel/framework/pull/32982)) -- Added `Illuminate\Filesystem\Filesystem::guessExtension()` method ([#33001](https://github.com/laravel/framework/pull/33001), [d26be90](https://github.com/laravel/framework/commit/d26be90df373dfd911029679b1765a46ae091d34)) -- Added `Illuminate\Http\Client\Request::toPsrRequest()` ([#33016](https://github.com/laravel/framework/pull/33016)) -- Added `Illuminate\Support\MessageBag::addIf()` method ([50efe09](https://github.com/laravel/framework/commit/50efe099b59e75563298deb992017b4cabfb021d)) -- Provide `psr/container-implementation` ([#33020](https://github.com/laravel/framework/pull/33020)) -- Support PHP 8's reflection API ([#33039](https://github.com/laravel/framework/pull/33039), [6018c1d](https://github.com/laravel/framework/commit/6018c1d18e7b764c17307c1f98d64482a00a668d)) - -### Fixed -- Restore `app()->getCached*Path()` absolute '/' behavior in Windows ([#32969](https://github.com/laravel/framework/pull/32969)) -- Fixed [Issue with using "sticky" option with Postgresql driver and read/write connections.](https://github.com/laravel/framework/issues/32966) ([#32973](https://github.com/laravel/framework/pull/32973)) -- Fixed custom class cast with dates ([2d52abc](https://github.com/laravel/framework/commit/2d52abc33865cc29b8e92a41ed7ad9a2b5383a11)) -- Fixed `Illuminate\Database\Eloquent\Collection::getQueueableRelations()` ([00e9ed7](https://github.com/laravel/framework/commit/00e9ed76483ea6ad1264676e7b1095b23e16a433)) -- Fixed bug with update existing pivot and polymorphic many to many ([684208b](https://github.com/laravel/framework/commit/684208b10460b49fa34354cc42f33b9b7135814f)) -- Fixed localization in tailwind view ([f2eb9ab](https://github.com/laravel/framework/commit/f2eb9ab82f7f5b126faf05241afe75e341fa22b1)) - -### Changed -- Use new line for `route:list` middleware ([#32993](https://github.com/laravel/framework/pull/32993)) -- Disallow generation commands with reserved names ([#33037](https://github.com/laravel/framework/pull/33037)) - - -## [v7.13.0 (2020-05-26)](https://github.com/laravel/framework/compare/v7.12.0...v7.13.0) - -### Added -- Added `Illuminate\Pagination\AbstractPaginator::useTailwind()` ([2279b73](https://github.com/laravel/framework/commit/2279b73d5553c34c970128264a248f3bb57afad6), [bf1eef4](https://github.com/laravel/framework/commit/bf1eef400951dcee04839a9ab7c15da1a807f89c), [13a9ec3](https://github.com/laravel/framework/commit/13a9ec349b8bcaa31d1757752ae0304f0328e5ce)) - -### Fixed -- Fixed route list command for excluded middleware ([7ebd211](https://github.com/laravel/framework/commit/7ebd21193df520d78269d7abd740537a2fae889e)) -- Fixed behavior of oneachside = 1 with paginator in `Illuminate\Pagination\UrlWindow` ([c59cffa](https://github.com/laravel/framework/commit/c59cffa7825498e1d419d8c86cd8527520f718cb), [5d817be](https://github.com/laravel/framework/commit/5d817bef236559cc9368e1ec4ceafa8a790f751d)) - -### Changed -- Using an indexed array as the limit modifier for phpredis zrangebyscore ([#32952](https://github.com/laravel/framework/pull/32952)) - - -## [v7.12.0 (2020-05-19)](https://github.com/laravel/framework/compare/v7.11.0...v7.12.0) - -### Added -- Added `Illuminate\Http\Middleware\TrustHosts` ([9229264](https://github.com/laravel/framework/commit/92292649621f2aadc84ab94376244650a9f55696)) -- Added ability to skip middleware from resource routes ([#32891](https://github.com/laravel/framework/pull/32891)) - -### Fixed -- Fixed Queued Mail MessageSent Listener With Attachments ([#32795](https://github.com/laravel/framework/pull/32795)) -- Added error clearing before sending in `Illuminate\Mail\Mailer::sendSwiftMessage()` ([#32799](https://github.com/laravel/framework/pull/32799)) -- Avoid foundation function call in the auth component ([#32805](https://github.com/laravel/framework/pull/32805)) -- Fixed inferred table reference for `Illuminate\Database\Schema\ForeignIdColumnDefinition::constrained()` ([#32847](https://github.com/laravel/framework/pull/32847)) -- Fixed wrong component generation ([73060db](https://github.com/laravel/framework/commit/73060db7c5541fadf5e4f2874a89d18621d705a3)) -- Fixed bug with request rebind and url defaults in `Illuminate\Routing\UrlGenerator` ([6ad92bf](https://github.com/laravel/framework/commit/6ad92bf9a8552a7759a7757cf821b01969baf0b6)) -- Fixed `Illuminate\Cache\ArrayStore::increment()` bug that changes expiration to forever ([#32875](https://github.com/laravel/framework/pull/32875)) - -### Changed -- Dont cache non objects in `Illuminate/Database/Eloquent/Concerns/HasAttributes::getClassCastableAttributeValue()` ([894fe22](https://github.com/laravel/framework/commit/894fe22c6c111b224de5bada24dcbba4c93f0305)) -- Added explicit `symfony/polyfill-php73` dependency ([5796b1e](https://github.com/laravel/framework/commit/5796b1e43dfe14914050a7e5dd24ddf803ec99b8)) -- Set `Cache\FileStore` file permissions only once ([#32845](https://github.com/laravel/framework/pull/32845), [11c533b](https://github.com/laravel/framework/commit/11c533b9aa062f4cba1dd0fe3673bf33d275480f)) -- Added alias as key of package's view components ([#32863](https://github.com/laravel/framework/pull/32863)) - - -## [v7.11.0 (2020-05-12)](https://github.com/laravel/framework/compare/v7.10.3...v7.11.0) - -### Added -- Added support for FILTER_FLAG_EMAIL_UNICODE via "email:filter_unicode" in email validator ([#32711](https://github.com/laravel/framework/pull/32711), [43a1ed1](https://github.com/laravel/framework/commit/43a1ed1ee272b77547d292af7d337c745cccd48a)) -- Added `Illuminate\Support\Stringable::split()` ([#32713](https://github.com/laravel/framework/pull/32713), [19c5054](https://github.com/laravel/framework/commit/19c5054eff4d00d234cd928db1e085aaa14c4692)) -- Added `orWhereIntegerInRaw()` and `orWhereIntegerNotInRaw()` to `Illuminate\Database\Query\Builder` ([#32710](https://github.com/laravel/framework/pull/32710)) -- Added `Illuminate\Cache\DatabaseStore::add()` ([7fc452b](https://github.com/laravel/framework/commit/7fc452bd8d6cebd7e7a0c6cd057aea7d4e9a7fc0)) -- Implement env and production Blade directives ([#32742](https://github.com/laravel/framework/pull/32742)) -- Added `Illuminate\Database\Eloquent\Relations\MorphTo::morphWithCount()` method ([#32738](https://github.com/laravel/framework/pull/32738)) -- Added `Illuminate\Database\Eloquent\Collection::loadMorphCount()` method ([#32739](https://github.com/laravel/framework/pull/32739)) -- Added support `viaQueues` method for notifications ([e97d17c](https://github.com/laravel/framework/commit/e97d17cb6061600960bca2818f419bccca6f7da2)) -- Added `loadMorph` and `loadMorphCount` methods to `Illuminate\Database\Eloquent\Model` ([#32760](https://github.com/laravel/framework/pull/32760)) -- Added `Illuminate\Database\DatabaseManager::usingConnection()` method ([#32761](https://github.com/laravel/framework/pull/32761), [5f8c7de](https://github.com/laravel/framework/commit/5f8c7de58c5ba2cdb38ba50f1dfcc4c869d0e02d)) -- Added `Illuminate\Http\Client\PendingRequest::head()` method ([#32782](https://github.com/laravel/framework/pull/32782)) - -### Fixed -- Fixed belongsToMany child relationship solving ([c5e88be](https://github.com/laravel/framework/commit/c5e88be082bc690961889812360cd6c5ba983117)) -- Allow overriding the MySQL server version for strict mode ([#32708](https://github.com/laravel/framework/pull/32708)) -- Added boolean to types that don't need character options ([#32716](https://github.com/laravel/framework/pull/32716)) -- Fixed `Illuminate\Foundation\Testing\PendingCommand` that do not resolve 'OutputStyle::class' from the container ([#32687](https://github.com/laravel/framework/pull/32687)) -- Clear resolved event facade on `Illuminate\Foundation\Testing\Concerns\MocksApplicationServices::withoutEvents()` ([d1e7f85](https://github.com/laravel/framework/commit/d1e7f85dfd79abbe4f5e01818f620f6ecc67de4d)) -- Fixed `Illuminate\Database\Eloquent\Collection::getQueueableRelations()` for filtered collections ([#32747](https://github.com/laravel/framework/pull/32747)) -- Fixed `Illuminate\Database\Eloquent\Collection::loadCount` method to ensure count is set on all models ([#32740](https://github.com/laravel/framework/pull/32740)) -- Fixed deprecated "Doctrine/Common/Inflector/Inflector" class ([#32734](https://github.com/laravel/framework/pull/32734)) -- Fixed `Illuminate\Validation\Validator::getPrimaryAttribute()` ([#32775](https://github.com/laravel/framework/pull/32775)) -- Revert of ["Remove `strval` from `Illuminate/Validation/ValidationRuleParser::explodeWildcardRules()`"](https://github.com/laravel/framework/commit/1c76a6f3a80fa8f756740566dffd9fa1be65c123) ([52940cf](https://github.com/laravel/framework/commit/52940cf3275cfebd47ec008fd8ae5bc6d6a42dfd)) - -### Changed -- Updated user model var name in `make:policy` command ([#32748](https://github.com/laravel/framework/pull/32748)) -- Remove the undocumented dot keys support in validators ([#32764](https://github.com/laravel/framework/pull/32764)) - - -## [v7.10.3 (2020-05-06)](https://github.com/laravel/framework/compare/v7.10.2...v7.10.3) - -### Added -- Added `Illuminate\Http\Client\Response::failed()` ([#32699](https://github.com/laravel/framework/pull/32699)) -- Added SSL SYSCALL EOF as a lost connection message ([#32697](https://github.com/laravel/framework/pull/32697)) - -### Fixed -- Fixed `FakerGenerator` Unique caching issue ([#32703](https://github.com/laravel/framework/pull/32703)) -- Set/reset the select to from.* in `Illuminate/Database/Query/Builder::runPaginationCountQuery()` ([858f454](https://github.com/laravel/framework/commit/858f4544d5672bf277686bdb112b1ce055416413), [98a242e](https://github.com/laravel/framework/commit/98a242e21041462054b965e587c250ac7be4f912)) - - -## [v7.10.2 (2020-05-06)](https://github.com/laravel/framework/compare/v7.10.1...v7.10.2) - -### Fixed -- Updated `Illuminate\Database\Query\Builder::runPaginationCountQuery()` to support groupBy and sub-selects ([#32688](https://github.com/laravel/framework/pull/32688)) - - -## [v7.10.1 (2020-05-05)](https://github.com/laravel/framework/compare/v7.10.0...v7.10.1) - -### Fixed -- Fixed `Illuminate\Database\Eloquent\Collection::getQueueableRelations()` ([7b32460](https://github.com/laravel/framework/commit/7b32469420258e9e52b24b2ffa7f491e79a3a870)) - - -## [v7.10.0 (2020-05-05)](https://github.com/laravel/framework/compare/v7.9.2...v7.10.0) - -### Added -- Added `artisan make:cast` command ([#32594](https://github.com/laravel/framework/pull/32594)) -- Added `Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase::assertDatabaseCount()` ([#32597](https://github.com/laravel/framework/pull/32597)) -- Allow configuring the auth_mode for SMTP mail driver ([#32616](https://github.com/laravel/framework/pull/32616)) -- Added `hasNamedScope()` function to the Base Model ([#32622](https://github.com/laravel/framework/pull/32622), [#32631](https://github.com/laravel/framework/pull/32631)) -- Allow doing truth-test assertions with just a closure ([#32626](https://github.com/laravel/framework/pull/32626), [f69ad90](https://github.com/laravel/framework/commit/f69ad90b9d508b59a017d0e412d8228e71230a51), [22d6fca](https://github.com/laravel/framework/commit/22d6fcafba610364aabb2b8e5c385edf56ae0156)) -- Run pagination count as subquery for group by and havings ([#32624](https://github.com/laravel/framework/pull/32624)) -- Added Callbacks with Output to Console Schedule ([#32633](https://github.com/laravel/framework/pull/32633), [35a7883](https://github.com/laravel/framework/commit/35a788316a0bc20295abe048a1bc1aa34a729ec7), [8d8d620](https://github.com/laravel/framework/commit/8d8d62024188c870df9dec1eeac428089f44c18e)) -- Added `Cache::lock()` support for the database cache driver ([#32639](https://github.com/laravel/framework/pull/32639), [573831b](https://github.com/laravel/framework/commit/573831b5028aa440f555d1072672db5069f306d1)) -- Same-session ID request concurrency limiting ([#32636](https://github.com/laravel/framework/pull/32636)) -- Add `skipUntil` and `skipWhile` methods to the collections ([#32672](https://github.com/laravel/framework/pull/32672), [#32676](https://github.com/laravel/framework/pull/32676)) -- Support delete with limit on sqlsrv ([f16d325](https://github.com/laravel/framework/commit/f16d3256f93be71935ed86951e58f90b83912feb)) -- Added `mergeFillable()` and `mergeGuarded()` to `Model` ([#32679](https://github.com/laravel/framework/pull/32679)) - -### Fixed -- Prevents a memory leak in Faker ([2228233](https://github.com/laravel/framework/commit/222823377c936ab4cceeb1fa42db84821c04bff6)) -- Fixed setting component name and attributes ([#32599](https://github.com/laravel/framework/pull/32599), [f8ff3ca](https://github.com/laravel/framework/commit/f8ff3cae1ebf2865ef7263b88559c581d48cde6e)) -- Fixed `Illuminate\Foundation\Testing\TestResponse::assertSessionHasInput()` ([f0639fd](https://github.com/laravel/framework/commit/f0639fda45fc2874986fe409d944dde21d42c6f3)) -- Set relation connection on eager loaded MorphTo ([#32602](https://github.com/laravel/framework/pull/32602)) -- Filtering null's in `hasMorph()` ([#32614](https://github.com/laravel/framework/pull/32614)) -- Fixed `Illuminate\Foundation\Console\EventMakeCommand::alreadyExists()` ([7bba4bf](https://github.com/laravel/framework/commit/7bba4bfbedb85ee252464aa932414d5517240722)) -- Fixed `Illuminate\Console\Scheduling\Schedule::compileParameters()` ([cfc3ac9](https://github.com/laravel/framework/commit/cfc3ac9c8b0a593d264ae722ab90601fa4882d0e), [36e215d](https://github.com/laravel/framework/commit/36e215dd39cd757a8ffc6b17794de60476b2289d)) -- Fixed bug with model name in `Illuminate\Database\Eloquent\RelationNotFoundException::make()` ([f72a166](https://github.com/laravel/framework/commit/f72a1662ab64cc543c532941b1ab1279001af8e9)) -- Allow trashed through parents to be included in has many through queries ([#32609](https://github.com/laravel/framework/pull/32609)) - -### Changed -- Changed `Illuminate/Database/Eloquent/Relations/Concerns/AsPivot::fromRawAttributes()` ([6c502c1](https://github.com/laravel/framework/commit/6c502c1135082e8b25f2720931b19d36eeec8f41)) -- Restore оnly common relations ([#32613](https://github.com/laravel/framework/pull/32613), [d82f78b](https://github.com/laravel/framework/commit/d82f78b13631c4a04b9595099da0022ca3d8b94e), [48e4d60](https://github.com/laravel/framework/commit/48e4d602d4f8fe9304e8998c5893206f67504dbf)) -- Use single space if plain email is empty in `Illuminate\Mail\Mailer::addContent()` ([0557622](https://github.com/laravel/framework/commit/055762286132d545cbc064dce645562c0d51532f)) -- Remove wasted file read when loading package manifest in `Illuminate\Foundation\PackageManifest::getManifest()` ([#32646](https://github.com/laravel/framework/pull/32646)) -- Do not change `character` and `collation` for some columns on change ([fccdf7c](https://github.com/laravel/framework/commit/fccdf7c42d5ceb50985b3e8243d7ba650de996d6)) -- Use table name when resolving has many through / one relationships ([8d69454](https://github.com/laravel/framework/commit/8d69454575267840643289b8de27d615cfe4bb62)) - - -## [v7.9.2 (2020-04-28)](https://github.com/laravel/framework/compare/v7.9.1...v7.9.2) - -### Changed -- Extract `InvokableComponentVariable` class ([f1ef6e6](https://github.com/laravel/framework/commit/f1ef6e6c40028cdafb95fc53e950b6ef73030458)) -- Changed argument order in `Illuminate\View\Compilers\ComponentTagCompiler::__construct()` ([520544d](https://github.com/laravel/framework/commit/520544dc24772b421410a2528ba01fd47818eeea)) - - -## [v7.9.1 (2020-04-28)](https://github.com/laravel/framework/compare/v7.9.0...v7.9.1) - -### Added -- Added more proxy methods to deferred value from `Illuminate\View\Component::createInvokableVariable()` ([08c4012](https://github.com/laravel/framework/commit/08c40123a438e40ad82582fee7ddaa1ff056bb83)) - - -## [v7.9.0 (2020-04-28)](https://github.com/laravel/framework/compare/v7.8.1...v7.9.0) - -### Added -- Add pdo try again as lost connection message ([#32544](https://github.com/laravel/framework/pull/32544)) -- Compile Echos Within Blade Component Attributes ([#32558](https://github.com/laravel/framework/pull/32558)) -- Parameterless Component Methods Invokable With & Without Parens ([#32560](https://github.com/laravel/framework/pull/32560)) - -### Fixed -- Fixed `firstWhere` behavior for relations ([#32525](https://github.com/laravel/framework/pull/32525)) -- Added check to avoid endless loop in `MailManager::createTransport()` ([#32549](https://github.com/laravel/framework/pull/32549)) -- Fixed table prefixes with `compileDropDefaultConstraint()` ([#32554](https://github.com/laravel/framework/pull/32554)) -- Fixed boolean value in `Illuminate\Foundation\Testing\TestResponse::assertSessionHasErrors()` ([#32555](https://github.com/laravel/framework/pull/32555)) -- Fixed `Model::getOriginal()` with custom casts ([9e22c7c](https://github.com/laravel/framework/commit/9e22c7cfa629773eab981ccad13080c0f4cb81b2)) - -### Changed -- Added `withName` to `Illuminate\View\Component::ignoredMethods()` ([2e9eef2](https://github.com/laravel/framework/commit/2e9eef20a17a8b78493ae775ee95ed11349455d7)) - - -## [v7.8.1 (2020-04-24)](https://github.com/laravel/framework/compare/v7.8.0...v7.8.1) - -### Fixed -- Fixed `Illuminate\Http\Resources\Json\PaginatedResourceResponse::toResponse()` ([d460374](https://github.com/laravel/framework/commit/d4603749c03e03e224de3d867e88458599bb9d58)) - - -## [v7.8.0 (2020-04-24)](https://github.com/laravel/framework/compare/v7.7.1...v7.8.0) - -### Added -- Added `signedRoute()` and `temporarySignedRoute()` methods to `Illuminate\Routing\Redirector` ([#32489](https://github.com/laravel/framework/pull/32489)) -- Added `takeUntil` and `takeWhile` collection methods ([#32494](https://github.com/laravel/framework/pull/32494), [#32496](https://github.com/laravel/framework/pull/32496)) -- Added `Illuminate\Container\ContextualBindingBuilder::giveTagged()` ([#32514](https://github.com/laravel/framework/pull/32514)) -- Added methods `withFragment` and `withoutFragment` to `Illuminate\Http\RedirectResponse` ([11d6bef](https://github.com/laravel/framework/commit/11d6befb4ed8b306f7ed40a205539a20d4bebe16), [0099591](https://github.com/laravel/framework/commit/0099591d63c51f9139db957ad42f3e783c1d0d30), [42c67a1](https://github.com/laravel/framework/commit/42c67a156acd6e6d44595e973774ad96fdc03857), [a1e741a](https://github.com/laravel/framework/commit/a1e741a1709b3d4998995b76abd990a6c09a5841)) -- Added `exclude_without` validation rule ([4083ae5](https://github.com/laravel/framework/commit/4083ae57c6371c889de94df526bb849040bb895c)) - -### Fixed -- Fixed compiled route actions without a namespace ([#32512](https://github.com/laravel/framework/pull/32512)) -- Reset select bindings when setting select ([#32531](https://github.com/laravel/framework/pull/32531)) - -### Changed -- Added warn in `Illuminate/Support/Facades/Auth::routes()` when laravel/ui is not installed ([#32482](https://github.com/laravel/framework/pull/32482)) -- Added auth to each master on `Illuminate\Redis\Connections\PhpRedisConnection::flushdb()` ([837921b](https://github.com/laravel/framework/commit/837921b23311e875a9d22c296a9193a1cd8205cb)) -- Register opis key so it is not tied to a deferred service provider (Illuminate/Encryption/EncryptionServiceProvider.php) ([62d8a07](https://github.com/laravel/framework/commit/62d8a0772553f3dff2d52a3ab062182c5efd75a2)) -- Pass status code to schedule finish ([#32516](https://github.com/laravel/framework/pull/32516)) -- Check route:list --columns option case insensitively ([#32521](https://github.com/laravel/framework/pull/32521)) - -### Deprecated -- Deprecate `Illuminate\Support\Traits\EnumeratesValues::until` ([#32517](https://github.com/laravel/framework/pull/32517)) - - -## [v7.7.1 (2020-04-21)](https://github.com/laravel/framework/compare/v7.7.0...v7.7.1) - -### Added -- Allow developers to specify accepted keys in array rule ([#32452](https://github.com/laravel/framework/pull/32452)) - -### Changed -- Add check is_object to `Illuminate\Database\Eloquent\Model::refresh()` ([1b0bdb4](https://github.com/laravel/framework/commit/1b0bdb43062a2792befe6fd754140124a8e4dc35)) - - -## [v7.7.0 (2020-04-21)](https://github.com/laravel/framework/compare/v7.6.2...v7.7.0) - -### Added -- Added ArrayAccess support for Http client get requests ([#32401](https://github.com/laravel/framework/pull/32401)) -- Added `Illuminate\Http\Client\Factory::assertSentCount()` ([#32407](https://github.com/laravel/framework/pull/32407)) -- Added `Illuminate\Database\Schema\Blueprint::rawIndex()` ([#32411](https://github.com/laravel/framework/pull/32411)) -- Added getGrammar into passthru in Eloquent builder ([#32412](https://github.com/laravel/framework/pull/32412)) -- Added `--relative` option to `storage:link` command ([#32457](https://github.com/laravel/framework/pull/32457), [24b705e](https://github.com/laravel/framework/commit/24b705e105d22df014bee3aab7ff12272457771e)) -- Added dynamic `column` key for foreign constraints ([#32449](https://github.com/laravel/framework/pull/32449)) -- Added container support for variadic constructor arguments ([#32454](https://github.com/laravel/framework/pull/32454), [1dd6db3](https://github.com/laravel/framework/commit/1dd6db3f2f22b1c65d13b3cbd58561f69aa4b317)) -- Added `Illuminate\Http\Client\Request::hasHeaders()` ([#32462](https://github.com/laravel/framework/pull/32462)) - -### Fixed -- Fixed `MorphPivot::delete()` for models with primary key ([#32421](https://github.com/laravel/framework/pull/32421)) -- Throw exception on missing required parameter on Container call method ([#32439](https://github.com/laravel/framework/pull/32439), [44c2a8d](https://github.com/laravel/framework/commit/44c2a8dc527f87f5a7fc59058df0f874a23449fa)) -- Fixed Http Client multipart request ([#32428](https://github.com/laravel/framework/pull/32428), [1f163d4](https://github.com/laravel/framework/commit/1f163d471b973b237772bb11cdcb994aadd3d530)) -- Fixed `Illuminate\Support\Stringable::isEmpty()` ([#32447](https://github.com/laravel/framework/pull/32447)) -- Fixed `whereNull`/`whereNotNull` for json in MySQL ([#32417](https://github.com/laravel/framework/pull/32417), [d3bb329](https://github.com/laravel/framework/commit/d3bb329ce40e716e8e92aa7c27a929be60511a97)) -- Fixed `Collection::orderBy()` with callable ([#32471](https://github.com/laravel/framework/pull/32471)) - -### Changed -- Re-use `Router::newRoute()` inside `CompiledRouteCollection` ([#32416](https://github.com/laravel/framework/pull/32416)) -- Make `Illuminate\Queue\InteractsWithQueue.php::$job` public ([2e272ee](https://github.com/laravel/framework/commit/2e272ee6df6ac22675a4645cac8b581017aac53f)) -- Catch and report exceptions thrown during schedule run execution ([#32461](https://github.com/laravel/framework/pull/32461)) - - -## [v7.6.2 (2020-04-15)](https://github.com/laravel/framework/compare/v7.6.1...v7.6.2) - -### Added -- Added `substrCount()` method to `Stringable` and `Str` ([#32393](https://github.com/laravel/framework/pull/32393)) - -### Fixed -- Fixed Lazyload `PackageManifest` ([#32391](https://github.com/laravel/framework/pull/32391)) -- Fixed email validator ([#32388](https://github.com/laravel/framework/pull/32388)) -- Fixed `Illuminate\Mail\Mailable::attachFromStorageDisk()` ([#32394](https://github.com/laravel/framework/pull/32394)) - -### Changed -- Changed `Illuminate\Translation\Translator::setLocale()` ([e78d24f](https://github.com/laravel/framework/commit/e78d24f31b84cd81c30b5d8837731d77ec089761), [a0094a5](https://github.com/laravel/framework/commit/a0094a57717b1f4c3e2a6feb978cc14f2c4690ff)) -- Changed `Illuminate\Mail\Mailable::attachData()` ([#32392](https://github.com/laravel/framework/pull/32392)) - - -## [v7.6.1 (2020-04-14)](https://github.com/laravel/framework/compare/v7.6.0...v7.6.1) - -### Fixed -- Fixed `Illuminate\Testing\TestResponse::offsetExists()` ([#32377](https://github.com/laravel/framework/pull/32377)) - - -## [v7.6.0 (2020-04-14)](https://github.com/laravel/framework/compare/v7.5.2...v7.6.0) - -### Added -- Added `Collection::until()` method ([#32262](https://github.com/laravel/framework/pull/32262)) -- Added `HtmlString::isEmpty()` method ([#32289](https://github.com/laravel/framework/pull/32289), [#32300](https://github.com/laravel/framework/pull/32300)) -- Added `Illuminate\Support\Stringable::isNotEmpty()` method ([#32293](https://github.com/laravel/framework/pull/32293)) -- Added `ltrim()` and `rtrim()` methods to `Illuminate\Support\Stringable` class ([#32288](https://github.com/laravel/framework/pull/32288)) -- Added ability to skip a middleware ([#32347](https://github.com/laravel/framework/pull/32347), [412261c](https://github.com/laravel/framework/commit/412261c180a0ffb561078b7f0647f2a0a5c46c8d)) -- Added `Illuminate\Http\Client\Response::object()` method ([#32341](https://github.com/laravel/framework/pull/32341)) -- Set component alias name ([#32346](https://github.com/laravel/framework/pull/32346)) -- Added `Illuminate\Database\Eloquent\Collection::append()` method ([#32324](https://github.com/laravel/framework/pull/32324)) -- Added "between" clauses for BelongsToMany pivot columns ([#32364](https://github.com/laravel/framework/pull/32364)) -- Support `retryAfter()` method option on Queued Listeners ([#32370](https://github.com/laravel/framework/pull/32370)) -- Added support for the new composer installed.json format ([#32310](https://github.com/laravel/framework/pull/32310)) -- Added `uuid` change support in migrations ([#32316](https://github.com/laravel/framework/pull/32316)) -- Allowed store resource into postgresql bytea ([#32319](https://github.com/laravel/framework/pull/32319)) - -### Fixed -- Fixed `*scan` methods for phpredis ([#32336](https://github.com/laravel/framework/pull/32336)) -- Fixed `Illuminate\Auth\Notifications\ResetPassword::toMail()` ([#32345](https://github.com/laravel/framework/pull/32345)) -- Call setLocale in `Illuminate\Translation\Translator::__construct()` ([1c6a504](https://github.com/laravel/framework/commit/1c6a50424c5558782a55769a226ab834484282e1)) -- Used a map to prevent unnecessary array access in `Illuminate\Http\Resources\Json\PaginatedResourceResponse::toResponse()` ([#32296](https://github.com/laravel/framework/pull/32296)) -- Prevent timestamp update when pivot is not dirty ([#32311](https://github.com/laravel/framework/pull/32311)) -- Fixed CURRENT_TIMESTAMP precision bug in `Illuminate\Database\Schema\Grammars\MySqlGrammar` ([#32298](https://github.com/laravel/framework/pull/32298)) - -### Changed -- Added default value to `HtmlString` constructor ([#32290](https://github.com/laravel/framework/pull/32290)) -- Used `BindingResolutionException` to signal problem with container resolution ([#32349](https://github.com/laravel/framework/pull/32349)) -- `Illuminate\Validation\Concerns\ValidatesAttributes.php ::validateUrl()` use Symfony/Validator 5.0.7 regex ([#32315](https://github.com/laravel/framework/pull/32315)) - -### Depreciated -- Depreciate the `elixir` function ([#32366](https://github.com/laravel/framework/pull/32366)) - - -## [v7.5.2 (2020-04-08)](https://github.com/laravel/framework/compare/v7.5.1...v7.5.2) - -### Fixed -- Prevent insecure characters in locale ([c248521](https://github.com/laravel/framework/commit/c248521f502c74c6cea7b0d221639d4aa752d5db)) - -### Optimization -- Optimize `Arr::set()` method ([#32282](https://github.com/laravel/framework/pull/32282)) - - -## [v7.5.1 (2020-04-07)](https://github.com/laravel/framework/compare/v7.5.0...v7.5.1) - -### Fixed -- Fixed Check a request header with an array value in `Illuminate\Http\Client\Request::hasHeader()` ([#32274](https://github.com/laravel/framework/pull/32274)) -- Fixed setting mail header ([#32272](https://github.com/laravel/framework/pull/32272)) - - -## [v7.5.0 (2020-04-07)](https://github.com/laravel/framework/compare/v7.4.0...v7.5.0) - -### Added -- Added `assertNotSent()` and `assertNothingSent()` methods to `Illuminate\Http\Client\Factory` ([#32197](https://github.com/laravel/framework/pull/32197)) -- Added enum support for `renameColumn()` ([#32205](https://github.com/laravel/framework/pull/32205)) -- Support returning an instance of a caster ([#32225](https://github.com/laravel/framework/pull/32225)) - -### Fixed -- Prevent long URLs from breaking email layouts ([#32189](https://github.com/laravel/framework/pull/32189)) -- Fixed camel casing relationship ([#32217](https://github.com/laravel/framework/pull/32217)) -- Fixed merging boolean or null attributes in Blade components ([#32245](https://github.com/laravel/framework/pull/32245)) -- Fixed Console expectation assertion order ([#32258](https://github.com/laravel/framework/pull/32258)) -- Fixed `route` helper with custom binding key ([#32264](https://github.com/laravel/framework/pull/32264)) -- Fixed double slashes matching in UriValidator (fix inconsistencies between cached and none cached routes) ([#32260](https://github.com/laravel/framework/pull/32260)) -- Fixed setting mail header ([#32272](https://github.com/laravel/framework/pull/32272)) - -### Optimization -- Optimize `Container::resolve()` method ([#32194](https://github.com/laravel/framework/pull/32194)) -- Optimize performance for `data_get()` method ([#32192](https://github.com/laravel/framework/pull/32192)) -- Optimize `Str::startsWith()` ([#32243](https://github.com/laravel/framework/pull/32243)) - - -## [v7.4.0 (2020-03-31)](https://github.com/laravel/framework/compare/v7.3.0...v7.4.0) - -### Added -- Makes the stubs used for `make:policy` customizable ([#32040](https://github.com/laravel/framework/pull/32040), [9d36a36](https://github.com/laravel/framework/commit/9d36a369d377044d0f468d1f02fa317cbb93571f)) -- Implement `HigherOrderWhenProxy` for Collections ([#32148](https://github.com/laravel/framework/pull/32148)) -- Added `Illuminate\Testing\PendingCommand::expectsChoice()` ([#32139](https://github.com/laravel/framework/pull/32139)) -- Added support for default values for the "props" blade tag ([#32177](https://github.com/laravel/framework/pull/32177)) -- Added `Castable` interface ([#32129](https://github.com/laravel/framework/pull/32129), [9cbf908](https://github.com/laravel/framework/commit/9cbf908c218bba74fbf83a83740b5c9f21c13e4e), [651371a](https://github.com/laravel/framework/commit/651371a2a982c06654b4df9af56110b666b2157f)) -- Added the ability to remove orders from the query builder ([#32186](https://github.com/laravel/framework/pull/32186)) - -### Fixed -- Added missing return in the `PendingMailFake::sendNow()` and `PendingMailFake::send()` ([#32093](https://github.com/laravel/framework/pull/32093)) -- Fixed custom Model attributes casts ([#32118](https://github.com/laravel/framework/pull/32118)) -- Fixed route group prefixing ([#32135](https://github.com/laravel/framework/pull/32135), [870efef](https://github.com/laravel/framework/commit/870efef4c23ff7f151b6e1f267ac18951a3af2f1)) -- Fixed component class view reference ([#32132](https://github.com/laravel/framework/pull/32132)) - -### Changed -- Remove Swift Mailer bindings ([#32165](https://github.com/laravel/framework/pull/32165)) -- Publish console stub when running `stub:publish` command ([#32096](https://github.com/laravel/framework/pull/32096)) -- Publish rule stub when running `make:rule` command ([#32097](https://github.com/laravel/framework/pull/32097)) -- Adding the middleware.stub to the files that will be published when running php artisan `stub:publish` ([#32099](https://github.com/laravel/framework/pull/32099)) -- Adding the factory.stub to the files that will be published when running php artisan `stub:publish` ([#32100](https://github.com/laravel/framework/pull/32100)) -- Adding the seeder.stub to the files that will be published when running php artisan `stub:publish` ([#32122](https://github.com/laravel/framework/pull/32122)) - - -## [v7.3.0 (2020-03-24)](https://github.com/laravel/framework/compare/v7.2.2...v7.3.0) - -### Added -- Added possibility to use `^4.0` versions of `ramsey/uuid` ([#32086](https://github.com/laravel/framework/pull/32086)) - -### Fixed -- Corrected suggested dependencies ([#32072](https://github.com/laravel/framework/pull/32072), [c01a70e](https://github.com/laravel/framework/commit/c01a70e33198e81d06d4b581e36e25a80acf8a68)) -- Avoid deadlock in test when sharing process group ([#32067](https://github.com/laravel/framework/pull/32067)) - - -## [v7.2.2 (2020-03-20)](https://github.com/laravel/framework/compare/v7.2.1...v7.2.2) - -### Fixed -- Fixed empty data for blade components ([#32032](https://github.com/laravel/framework/pull/32032)) -- Fixed subdirectories when making components by `make:component` ([#32030](https://github.com/laravel/framework/pull/32030)) -- Fixed serialization of models when sending notifications ([#32051](https://github.com/laravel/framework/pull/32051)) -- Fixed route trailing slash in cached routes matcher ([#32048](https://github.com/laravel/framework/pull/32048)) - -### Changed -- Throw exception for non existing component alias ([#32036](https://github.com/laravel/framework/pull/32036)) -- Don't overwrite published stub files by default in `stub:publish` command ([#32038](https://github.com/laravel/framework/pull/32038)) - - -## [v7.2.1 (2020-03-19)](https://github.com/laravel/framework/compare/v7.2.0...v7.2.1) - -### Fixed -- Enabling Windows absolute cache paths normalizing ([#31985](https://github.com/laravel/framework/pull/31985), [adfcb59](https://github.com/laravel/framework/commit/adfcb593fef058a32398d1e84d9083c8c5f893ac)) -- Fixed blade newlines ([#32026](https://github.com/laravel/framework/pull/32026)) -- Fixed exception rendering in debug mode ([#32027](https://github.com/laravel/framework/pull/32027)) -- Fixed route naming issue ([#32028](https://github.com/laravel/framework/pull/32028)) - - -## [v7.2.0 (2020-03-17)](https://github.com/laravel/framework/compare/v7.1.3...v7.2.0) - -### Added -- Added `Illuminate\Testing\PendingCommand::expectsConfirmation()` ([#31965](https://github.com/laravel/framework/pull/31965)) -- Allowed configuring the timeout for the smtp mail driver ([#31973](https://github.com/laravel/framework/pull/31973)) -- Added `Http client` query string support ([#31996](https://github.com/laravel/framework/pull/31996)) - -### Fixed -- Fixed `cookie` helper signature , matching match `CookieFactory` ([#31974](https://github.com/laravel/framework/pull/31974)) -- Added missing `ramsey/uuid` dependency to `Illuminate/Queue/composer.json` ([#31988](https://github.com/laravel/framework/pull/31988)) -- Fixed output of component attributes in View ([#31994](https://github.com/laravel/framework/pull/31994)) - -### Changed -- Publish the form request stub used by RequestMakeCommand ([#31962](https://github.com/laravel/framework/pull/31962)) -- Handle prefix update on route level prefix ([449c80](https://github.com/laravel/framework/commit/449c8056cc0f13e7e20428700045339bae6bdca2)) -- Ensure SqsQueue queues are only suffixed once ([#31925](https://github.com/laravel/framework/pull/31925)) -- Added space after component closing tag for the View ([#32005](https://github.com/laravel/framework/pull/32005)) - - -## [v7.1.3 (2020-03-14)](https://github.com/laravel/framework/compare/v7.1.2...v7.1.3) - -### Fixed -- Unset `pivotParent` on `Pivot::unsetRelations()` ([#31956](https://github.com/laravel/framework/pull/31956)) - -### Changed -- Escape merged attributes by default in `Illuminate\View\ComponentAttributeBag` ([83c8e6e](https://github.com/laravel/framework/commit/83c8e6e6b575d0029ea164ba4b44f4c4895dbb3d)) - - -## [v7.1.2 (2020-03-13)](https://github.com/laravel/framework/compare/v7.1.1...v7.1.2) - -### Fixed -- Corrected suggested dependencies ([bb0ec42](https://github.com/laravel/framework/commit/bb0ec42b5a55b3ebf3a5a35cc6df01eec290dfa9)) -- Fixed null value injected from container in routes ([#31867](https://github.com/laravel/framework/pull/31867), [c666c42](https://github.com/laravel/framework/commit/c666c424e8a60539a8fbd7cb5a3474785d9db22a)) - -### Changed -- Escape attributes automatically in some situations in `Illuminate\View\Compilers\ComponentTagCompiler` ([#31945](https://github.com/laravel/framework/pull/31945)) - - -## [v7.1.1 (2020-03-12)](https://github.com/laravel/framework/compare/v7.1.0...v7.1.1) - -### Added -- Added `dispatchToQueue()` to `BusFake` ([#31935](https://github.com/laravel/framework/pull/31935)) -- Support either order of arguments for symmetry with livewire ([8d558670](https://github.com/laravel/framework/commit/8d5586700ad97b92ac622ea72c1fefe52c359265)) - -### Fixed -- Bring `--daemon` option back to `queue:work` command ([24c1818](https://github.com/laravel/framework/commit/24c18182a82ee24be62d2ac1c6793c237944cda8)) -- Fixed scheduler dependency assumptions ([#31894](https://github.com/laravel/framework/pull/31894)) -- Fixed ComponentAttributeBag merge behaviour ([#31932](https://github.com/laravel/framework/pull/31932)) - -### Changed -- Intelligently drop unnamed prefix name routes when caching ([#31917](https://github.com/laravel/framework/pull/31917)) -- Closure jobs needs illuminate/queue ([#31933](https://github.com/laravel/framework/pull/31933)) -- Have a cache aware interface instead of concrete checks ([#31903](https://github.com/laravel/framework/pull/31903)) - - -## [v7.1.0 (2020-03-10)](https://github.com/laravel/framework/compare/v7.0.8...v7.1.0) - -### Added -- Added `Illuminate\Routing\RouteRegistrar::apiResource()` method ([#31857](https://github.com/laravel/framework/pull/31857)) -- Added optional $table parameter to `ForeignIdColumnDefinition::constrained()` method ([#31853](https://github.com/laravel/framework/pull/31853)) - -### Fixed -- Fixed phpredis "zadd" and "exists" on cluster ([#31838](https://github.com/laravel/framework/pull/31838)) -- Fixed trailing slash in `Illuminate\Routing\CompiledRouteCollection::match()` ([3d58cd9](https://github.com/laravel/framework/commit/3d58cd91d6ec483a43a4c23af9b75ecdd4a358de), [ac6f3a8](https://github.com/laravel/framework/commit/ac6f3a8bd0e94ea1319b6f278ecf7f3f8bada3c2)) -- Fixed "srid" mysql schema ([#31852](https://github.com/laravel/framework/pull/31852)) -- Fixed Microsoft ODBC lost connection handling ([#31879](https://github.com/laravel/framework/pull/31879)) - -### Changed -- Fire `MessageLogged` event after the message has been logged (not before) ([#31843](https://github.com/laravel/framework/pull/31843)) -- Avoid using array_merge_recursive in HTTP client ([#31858](https://github.com/laravel/framework/pull/31858)) -- Expire the jobs cache keys after 1 day ([#31854](https://github.com/laravel/framework/pull/31854)) -- Avoid global app() when compiling components ([#31868](https://github.com/laravel/framework/pull/31868)) - - -## [v7.0.8 (2020-03-08)](https://github.com/laravel/framework/compare/v7.0.7...v7.0.8) - -### Added -- Added `Illuminate\Mail\Mailable::when()` method ([#31828](https://github.com/laravel/framework/pull/31828)) - -### Fixed -- Match Symfony's `Command::setHidden` declaration ([#31840](https://github.com/laravel/framework/pull/31840)) -- Fixed dynamically adding of routes during caching ([#31829](https://github.com/laravel/framework/pull/31829)) - -### Changed -- Update the encryption algorithm to provide deterministic encryption sizes ([#31721](https://github.com/laravel/framework/pull/31721)) - - -## [v7.0.7 (2020-03-07)](https://github.com/laravel/framework/compare/v7.0.6...v7.0.7) - -### Fixed -- Fixed type hint for `Request::get()` method ([#31826](https://github.com/laravel/framework/pull/31826)) -- Add missing public methods to `Illuminate\Routing\RouteCollectionInterface` ([e4f477c](https://github.com/laravel/framework/commit/e4f477c42d3e24f6cdf44a45801c0db476ad2b91)) - - -## [v7.0.6 (2020-03-06)](https://github.com/laravel/framework/compare/v7.0.5...v7.0.6) - -### Added -- Added queue suffix for SQS driver ([#31784](https://github.com/laravel/framework/pull/31784)) - -### Fixed -- Fixed model binding when route cached ([af80685](https://github.com/laravel/framework/commit/af806851931700e8dd8de0ac0333efd853b19f3d)) -- Fixed incompatible `Factory` contract for `MailFacade` ([#31809](https://github.com/laravel/framework/pull/31809)) - -### Changed -- Fixed typehints in `Illuminate\Foundation\Application::handle()` ([#31806](https://github.com/laravel/framework/pull/31806)) - - -## [v7.0.5 (2020-03-06)](https://github.com/laravel/framework/compare/v7.0.4...v7.0.5) - -### Fixed -- Fixed `Illuminate\Http\Client\PendingRequest::withCookies()` method ([36d783c](https://github.com/laravel/framework/commit/36d783ce8dbd8736e694ff60ae66e542c62411c3)) -- Catch Symfony `MethodNotAllowedException` exception in `CompiledRouteCollection::match()` method ([#31762](https://github.com/laravel/framework/pull/31762)) -- Fixed a bug with slash prefix in the route ([#31760](https://github.com/laravel/framework/pull/31760)) -- Fixed root URI not showing in the `route:list` ([#31771](https://github.com/laravel/framework/pull/31771)) -- Fixed model restoring right after being soft deleting ([#31719](https://github.com/laravel/framework/pull/31719)) -- Fixed array lock release behavior ([#31795](https://github.com/laravel/framework/pull/31795)) -- Fixed `Illuminate\Support\Str::slug()` method ([e4f22d8](https://github.com/laravel/framework/commit/e4f22d855b429e4141885d542438c859f84bfe49)) - -### Changed -- Throw exception for duplicate route names in `Illuminate\Routing\AbstractRouteCollection::addToSymfonyRoutesCollection()` method ([#31755](https://github.com/laravel/framework/pull/31755)) -- Revert disabling expired views checks ([#31798](https://github.com/laravel/framework/pull/31798)) - - -## [v7.0.4 (2020-03-05)](https://github.com/laravel/framework/compare/v7.0.3...v7.0.4) - -### Changed -- Changed of route prefix parameter parsing ([b38e179](https://github.com/laravel/framework/commit/b38e179642d6a76a7713ced1fddde841900ac3ad)) - - -## [v7.0.3 (2020-03-04)](https://github.com/laravel/framework/compare/v7.0.2...v7.0.3) - -### Fixed -- Fixed route caching attempt in `Illuminate\Routing\CompiledRouteCollection::newRoute()` ([90b0167](https://github.com/laravel/framework/commit/90b0167d97e61eb06fce9cfc58527f4e09cd2a5e)) -- Catch Symfony exception in `CompiledRouteCollection::match()` method ([#31738](https://github.com/laravel/framework/pull/31738)) -- Fixed Eloquent model casting ([2b395cd](https://github.com/laravel/framework/commit/2b395cd1f2fe95b67edf97684f09b7c5c4a55152)) -- Fixed `UrlGenerator` constructor ([#31740](https://github.com/laravel/framework/pull/31740)) - -### Changed -- Added message to `Illuminate\Http\Client\RequestException` ([#31720](https://github.com/laravel/framework/pull/31720)) - - -## [v7.0.2 (2020-03-04)](https://github.com/laravel/framework/compare/v7.0.1...v7.0.2) - -### Fixed -- Fixed `ascii()` \ `isAscii()` \ `slug()` methods on the `Str` class with null value in the methods ([#31717](https://github.com/laravel/framework/pull/31717)) -- Fixed `trim` of the prefix in the `CompiledRouteCollection::newRoute()` ([ce0355c](https://github.com/laravel/framework/commit/ce0355c72bf4defb93ae80c7bf7812bd6532031a), [b842c65](https://github.com/laravel/framework/commit/b842c65ecfe1ea7839d61a46b177b6b5887fd4d2)) - -### Changed -- remove comments before compiling components in the `BladeCompiler` ([2964d2d](https://github.com/laravel/framework/commit/2964d2dfd3cc50f7a709effee0af671c86587915)) - - -## [v7.0.1 (2020-03-03)](https://github.com/laravel/framework/compare/v7.0.0...v7.0.1) - -### Fixed -- Fixed `Illuminate\View\Component::withAttributes()` method ([c81ffad](https://github.com/laravel/framework/commit/c81ffad7ef8d74ebd109f399abbdc5c7ebabff88)) - - -## [v7.0.0 (2020-03-03)](https://github.com/laravel/framework/compare/v6.18.0...v7.0.0) - -Check the upgrade guide in the [Official Laravel Upgrade Documentation](https://laravel.com/docs/7.x/upgrade). Also you can see some release notes in the [Official Laravel Release Documentation](https://laravel.com/docs/7.x/releases). diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index 3f4502958dd7..6879551e0a1e 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,475 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.3.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.22.0...8.x) + + +## [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)) +- Added `Illuminate\Console\Concerns\CallsCommands::callSilently()` as alias for `callSilent()` ([7f3101b](https://github.com/laravel/framework/commit/7f3101bf6e8a0f048a243a55be7fc79eb359b609), [0294433](https://github.com/laravel/framework/commit/029443349294e3b6e7bebfe9c23a51a9821ec497)) +- Added option to release unique job locks before processing ([#35255](https://github.com/laravel/framework/pull/35255), [b53f13e](https://github.com/laravel/framework/commit/b53f13ef6c8625176defcb83d2fb8d4d5887d068)) +- Added ably broadcaster ([e0f3f8e](https://github.com/laravel/framework/commit/e0f3f8e8241e1ea34a3a3b8c543871cdc00290bf), [6381aa9](https://github.com/laravel/framework/commit/6381aa994756429156b7376e98606458b052b1d7)) +- Added ability to define table name as default morph type ([#35257](https://github.com/laravel/framework/pull/35257)) +- Allow overriding the MySQL server version for database queue driver ([#35263](https://github.com/laravel/framework/pull/35263)) +- Added `Illuminate\Foundation\Testing\Wormhole::back()` ([#35261](https://github.com/laravel/framework/pull/35261)) +- Support delaying notifications per channel ([#35273](https://github.com/laravel/framework/pull/35273)) +- Allow sorting on multiple criteria ([#35277](https://github.com/laravel/framework/pull/35277), [53eb307](https://github.com/laravel/framework/commit/53eb307fea077299d409adf3ba0307a8fda4c4d1)) +- Added `Illuminate/Database/Console/DbCommand.php` command ([#35304](https://github.com/laravel/framework/pull/35304), [b559b3e](https://github.com/laravel/framework/commit/b559b3e7c4995ef468b35e8a6117ef24fdeca053)) +- Added Collections `splitIn` methods ([#35295](https://github.com/laravel/framework/pull/35295)) + +### Fixed +- Fixed rendering of notifications with config custom theme ([325a335](https://github.com/laravel/framework/commit/325a335ccf45426eabb27131ed48aa6114434c99)) +- Fixing BroadcastException message in PusherBroadcaster@broadcast ([#35290](https://github.com/laravel/framework/pull/35290)) +- Fixed generic DetectsLostConnection string ([#35323](https://github.com/laravel/framework/pull/35323)) +- Fixed SQL Server command generation ([#35317](https://github.com/laravel/framework/pull/35317)) +- Fixed route model binding on cached closure routes ([eb3e262](https://github.com/laravel/framework/commit/eb3e262c870739a6e9705b851e0066b3473eed2b)) + +### Changed +- Disable CSRF on broadcast route ([acb4b77](https://github.com/laravel/framework/commit/acb4b77adc6e257e132e3b036abe1ec88885cfb7)) +- Easily set a null cache driver ([#35262](https://github.com/laravel/framework/pull/35262)) +- Updated `aws/aws-sdk-php` suggest to `^3.155` ([#35267](https://github.com/laravel/framework/pull/35267)) +- Ensure ShouldBeUniqueUntilProcessing job lock is released once ([#35270](https://github.com/laravel/framework/pull/35270)) +- Rename qualifyColumn to qualifyPivotColumn in BelongsToMany & MorphToMany ([#35276](https://github.com/laravel/framework/pull/35276)) +- Check if AsPivot trait is used instead of Pivot Model in `Illuminate\Database\Eloquent\Relations\BelongsToMany` ([#35271](https://github.com/laravel/framework/pull/35271)) +- Avoid no-op database query in Model::destroy() with empty ids ([#35294](https://github.com/laravel/framework/pull/35294)) +- Use --no-owner and --no-acl with pg_restore ([#35309](https://github.com/laravel/framework/pull/35309)) + + +## [v8.15.0 (2020-11-17)](https://github.com/laravel/framework/compare/v8.14.0...v8.15.0) + +### Added +- Added lock support for file and null cache drivers ([#35139](https://github.com/laravel/framework/pull/35139), [a345185](https://github.com/laravel/framework/commit/a3451859d1cff45fba423cf577d00f5b2b648c7a)) +- Added a `doesntExpectOutput` method for console command testing ([#35160](https://github.com/laravel/framework/pull/35160), [c90fc5f](https://github.com/laravel/framework/commit/c90fc5f6b8e91e3f6b0f2f3a74cad7d8a49bc71b)) +- Added support of MorphTo relationship eager loading constraints ([#35190](https://github.com/laravel/framework/pull/35190)) +- Added `Illuminate\Http\ResponseTrait::withoutCookie()` ([e9483c4](https://github.com/laravel/framework/commit/e9483c441d5f0c8598d438d6024db8b1a7aa55fe)) +- Use dynamic app namespace in Eloquent Factory instead of App\ string ([#35204](https://github.com/laravel/framework/pull/35204), [4885bd2](https://github.com/laravel/framework/commit/4885bd2d4ecf79de175d5308569ab0d608e8f55b)) +- Added `read` / `unread` scopes to database notifications ([#35215](https://github.com/laravel/framework/pull/35215)) +- Added `classBasename()` method to `Stringable` ([#35219](https://github.com/laravel/framework/pull/35219)) +- Added before resolving callbacks to container ([#35228](https://github.com/laravel/framework/pull/35228)) +- Adds the possibility of testing file upload content ([#35231](https://github.com/laravel/framework/pull/35231)) +- Added lost connection messages for MySQL persistent connections ([#35224](https://github.com/laravel/framework/pull/35224)) +- Added Support DBAL v3.0 ([#35236](https://github.com/laravel/framework/pull/35236)) + +### Fixed +- Update MySqlSchemaState.php to support MariaDB dump ([#35184](https://github.com/laravel/framework/pull/35184)) +- Fixed pivot and morphpivot fresh and refresh methods ([#35193](https://github.com/laravel/framework/pull/35193)) +- Fixed pivot restoration ([#35218](https://github.com/laravel/framework/pull/35218)) + +### Changed +- Updated `EmailVerificationRequest.php` to check if user is not already verified ([#35174](https://github.com/laravel/framework/pull/35174)) +- Make `Validator::parseNamedParameters()` public ([#35183](https://github.com/laravel/framework/pull/35183)) +- Ignore max attempts if retryUntil is set in `queue:work` ([#35214](https://github.com/laravel/framework/pull/35214)) +- Explode string channels on `Illuminate/Log/LogManager::createStackDriver()` ([e5b86f2](https://github.com/laravel/framework/commit/e5b86f2efec2959fb0e85ad5ee5de18f430643c4)) + + +## [v8.14.0 (2020-11-10)](https://github.com/laravel/framework/compare/v8.13.0...v8.14.0) + +### Added +- Added ability to dispatch unique jobs ([#35042](https://github.com/laravel/framework/pull/35042), [2123e60](https://github.com/laravel/framework/commit/2123e603af027e7590974864715c028357ea4969)) +- Added `Model::encryptUsing()` ([#35080](https://github.com/laravel/framework/pull/35080)) +- Added support to MySQL dump and import using socket ([#35083](https://github.com/laravel/framework/pull/35083), [c43054b](https://github.com/laravel/framework/commit/c43054b9decad4f66937c229e4ef0f32760c8611)) +- Allow custom broadcastWith in notification broadcast channel ([#35142](https://github.com/laravel/framework/pull/35142)) +- Added `Illuminate\Routing\CreatesRegularExpressionRouteConstraints::whereAlphaNumeric()` ([#35154](https://github.com/laravel/framework/pull/35154)) + +### Fixed +- Fixed typo in `make:seeder` command name inside ModelMakeCommand ([#35107](https://github.com/laravel/framework/pull/35107)) +- Fix SQL Server grammar for upsert (missing semicolon) ([#35112](https://github.com/laravel/framework/pull/35112)) +- Respect migration table name in config when dumping schema ([110eb15](https://github.com/laravel/framework/commit/110eb15a77f84da0d83ebc2bb123eec08ecc19ca)) +- Respect theme when previewing notification ([ed4411d](https://github.com/laravel/framework/commit/ed4411d310f259f75e95e882b748ba9d76d7cfad)) +- Fix appendable attributes in Blade components ([#35131](https://github.com/laravel/framework/pull/35131)) +- Remove decrypting array cookies from cookie decrypting ([#35130](https://github.com/laravel/framework/pull/35130)) +- Turn the eloquent collection into a base collection if mapWithKeys loses models ([#35129](https://github.com/laravel/framework/pull/35129)) + +### Changed +- Move dispatching of DatabaseRefreshed event to fire before seeders are run ([#35091](https://github.com/laravel/framework/pull/35091)) +- Handle returning false from reportable callback ([55f0b5e](https://github.com/laravel/framework/commit/55f0b5e7449b87b7340a761bf9e6456fdc8ffc4d)) +- Update `Illuminate\Database\Schema\Grammars\MySqlGrammar::typeTimestamp()` ([#35143](https://github.com/laravel/framework/pull/35143)) +- Remove expectedTables after converting to expectedOutput in PendingCommand ([#35163](https://github.com/laravel/framework/pull/35163)) +- Change SQLite schema command environment variables to work on Windows ([#35164](https://github.com/laravel/framework/pull/35164)) + + +## [v8.13.0 (2020-11-03)](https://github.com/laravel/framework/compare/v8.12.3...v8.13.0) + +### Added +- Added `loadMax()` | `loadMin()` | `loadSum()` | `loadAvg()` methods to `Illuminate\Database\Eloquent\Collection`. Added `loadMax()` | `loadMin()` | `loadSum()` | `loadAvg()` | `loadMorphMax()` | `loadMorphMin()` | `loadMorphSum()` | `loadMorphAvg()` methods to `Illuminate\Database\Eloquent\Model` ([#35029](https://github.com/laravel/framework/pull/35029)) +- Modify `Illuminate\Database\Eloquent\Concerns\QueriesRelationships::has()` method to support MorphTo relations ([#35050](https://github.com/laravel/framework/pull/35050)) +- Added `Illuminate\Support\Stringable::chunk()` ([#35038](https://github.com/laravel/framework/pull/35038)) + +### Fixed +- Fixed a few issues in `Illuminate\Database\Eloquent\Concerns\QueriesRelationships::withAggregate()` ([#35061](https://github.com/laravel/framework/pull/35061), [#35063](https://github.com/laravel/framework/pull/35063)) + +### Changed +- Set chain `queue` | `connection` | `delay` only when explicitly configured in ([#35047](https://github.com/laravel/framework/pull/35047)) + +### Refactoring +- Remove redundant unreachable return statements in some places ([#35053](https://github.com/laravel/framework/pull/35053)) + + +## [v8.12.3 (2020-10-30)](https://github.com/laravel/framework/compare/v8.12.2...v8.12.3) + +### Fixed +- Fixed `Illuminate\Database\Eloquent\Concerns\QueriesRelationships::withAggregate()` ([20b0c6e](https://github.com/laravel/framework/commit/20b0c6e19b635466f776502b3f1260c7c51b04ae)) + + +## [v8.12.2 (2020-10-29)](https://github.com/laravel/framework/compare/v8.12.1...v8.12.2) + +### Fixed +- [Add some fixes](https://github.com/laravel/framework/compare/v8.12.1...v8.12.2) + + +## [v8.12.1 (2020-10-29)](https://github.com/laravel/framework/compare/v8.12.0...v8.12.1) + +### Fixed +- Fixed alias usage in `Eloquent` ([6091048](https://github.com/laravel/framework/commit/609104806b8b639710268c75c22f43034c2b72db)) +- Fixed `Illuminate\Support\Reflector::isCallable()` ([a90f344](https://github.com/laravel/framework/commit/a90f344c66f0a5bb1d718f8bbd20c257d4de9e02)) + + +## [v8.12.0 (2020-10-29)](https://github.com/laravel/framework/compare/v8.11.2...v8.12.0) + +### Added +- Added ability to create observers with custom path via `make:observer` command ([#34911](https://github.com/laravel/framework/pull/34911)) +- Added `Illuminate\Database\Eloquent\Factories\Factory::lazy()` ([#34923](https://github.com/laravel/framework/pull/34923)) +- Added ability to make cast with custom stub file via `make:cast` command ([#34930](https://github.com/laravel/framework/pull/34930)) +- ADDED: Custom casts can implement increment/decrement logic ([#34964](https://github.com/laravel/framework/pull/34964)) +- Added encrypted Eloquent cast ([#34937](https://github.com/laravel/framework/pull/34937), [#34948](https://github.com/laravel/framework/pull/34948)) +- Added `DatabaseRefreshed` event to be emitted after database refreshed ([#34952](https://github.com/laravel/framework/pull/34952), [f31bfe2](https://github.com/laravel/framework/commit/f31bfe2fb83829a900f75fccd12af4b69ffb6275)) +- Added `withMax()`|`withMin()`|`withSum()`|`withAvg()` methods to `Illuminate/Database/Eloquent/Concerns/QueriesRelationships` ([#34965](https://github.com/laravel/framework/pull/34965), [f4e4d95](https://github.com/laravel/framework/commit/f4e4d95c8d4c2f63f9bd80c2a4cfa6b2c78bab1b), [#35004](https://github.com/laravel/framework/pull/35004)) +- Added `explain()` to `Query\Builder` and `Eloquent\Builder` ([#34969](https://github.com/laravel/framework/pull/34969)) +- Make `multiple_of` validation rule handle non-integer values ([#34971](https://github.com/laravel/framework/pull/34971)) +- Added `setKeysForSelectQuery` method and use it when refreshing model data in Models ([#34974](https://github.com/laravel/framework/pull/34974)) +- Full PHP 8.0 Support ([#33388](https://github.com/laravel/framework/pull/33388)) +- Added `Illuminate\Support\Reflector::isCallable()` ([#34994](https://github.com/laravel/framework/pull/34994), [8c16891](https://github.com/laravel/framework/commit/8c16891c6e7a4738d63788f4447614056ab5136e), [31917ab](https://github.com/laravel/framework/commit/31917abcfa0db6ec6221bb07fc91b6e768ff5ec8), [11cfa4d](https://github.com/laravel/framework/commit/11cfa4d4c92bf2f023544d58d51b35c5d31dece0), [#34999](https://github.com/laravel/framework/pull/34999)) +- Added route regex registration methods ([#34997](https://github.com/laravel/framework/pull/34997), [3d405cc](https://github.com/laravel/framework/commit/3d405cc2eb66bba97433b46abaca52623c64c94b), [c2df0d5](https://github.com/laravel/framework/commit/c2df0d5faddeb7e58d1832c1c1f0f309619969af)) +- Added dontRelease option to RateLimited and RateLimitedWithRedis job middleware ([#35010](https://github.com/laravel/framework/pull/35010)) + +### Fixed +- Fixed check of file path in `Illuminate\Database\Schema\PostgresSchemaState::load()` ([268237f](https://github.com/laravel/framework/commit/268237fcda420e5c26ab2f0fbdb9b8783c276ff8)) +- Fixed: `PhpRedis (v5.3.2)` cluster - set default connection context to `null` ([#34935](https://github.com/laravel/framework/pull/34935)) +- Fixed Eloquent Model `loadMorph` and `loadMorphCount` methods ([#34972](https://github.com/laravel/framework/pull/34972)) +- Fixed ambigious column on many to many with select load ([5007986](https://github.com/laravel/framework/commit/500798623d100a9746b2931ae6191cb756521f05)) +- Fixed Postgres Dump ([#35018](https://github.com/laravel/framework/pull/35018)) + +### Changed +- Changed `make:factory` command ([#34947](https://github.com/laravel/framework/pull/34947), [4f38176](https://github.com/laravel/framework/commit/4f3817654a6376a2f6cd59dc5fb529ebad1d951f)) +- Make assertSee, assertSeeText, assertDontSee and assertDontSeeText accept an array ([#34982](https://github.com/laravel/framework/pull/34982), [2b98bcc](https://github.com/laravel/framework/commit/2b98bcca598eb919b2afd61e5fb5cb86aec4c706)) + + +## [v8.11.2 (2020-10-20)](https://github.com/laravel/framework/compare/v8.11.1...v8.11.2) + +### Revert +- Revert ["Change loadRoutesFrom to accept $attributes](https://github.com/laravel/framework/pull/34866)" ([#34909](https://github.com/laravel/framework/pull/34909)) + + +## [v8.11.1 (2020-10-20)](https://github.com/laravel/framework/compare/v8.11.0...v8.11.1) + +### Fixed +- Fixed `bound()` method ([a7759d7](https://github.com/laravel/framework/commit/a7759d70e15b0be946569b8299ac694c08a35d7e)) + + +## [v8.11.0 (2020-10-20)](https://github.com/laravel/framework/compare/v8.10.0...v8.11.0) + +### Added +- Added job middleware to prevent overlapping jobs ([#34794](https://github.com/laravel/framework/pull/34794), [eed05b4](https://github.com/laravel/framework/commit/eed05b41097cfe62766d4086ede8dee97c057c29)) +- Bring Rate Limiters to Jobs ([#34829](https://github.com/laravel/framework/pull/34829), [ae00294](https://github.com/laravel/framework/commit/ae00294c418e431372bad0d09ac15d15925247f7)) +- Added `multiple_of` custom replacer in validator ([#34858](https://github.com/laravel/framework/pull/34858)) +- Preserve eloquent collection type after calling ->fresh() ([#34848](https://github.com/laravel/framework/pull/34848)) +- Provisional support for PHP 8.0 for 6.x (Changed some code in 8.x) ([#34884](https://github.com/laravel/framework/pull/34884), [28bb76e](https://github.com/laravel/framework/commit/28bb76efbcfc5fee57307ffa062b67ff709240dc)) + +### Fixed +- Fixed `fresh()` and `refresh()` on pivots and morph pivots ([#34836](https://github.com/laravel/framework/pull/34836)) +- Fixed config `batching` typo ([#34852](https://github.com/laravel/framework/pull/34852)) +- Fixed `Illuminate\Queue\Console\RetryBatchCommand` for un-found batch id ([#34878](https://github.com/laravel/framework/pull/34878)) + +### Changed +- Change `loadRoutesFrom()` to accept group $attributes ([#34866](https://github.com/laravel/framework/pull/34866)) + + +## [v8.10.0 (2020-10-13)](https://github.com/laravel/framework/compare/v8.9.0...v8.10.0) + +### Added +- Allow for chains to be added to batches ([#34612](https://github.com/laravel/framework/pull/34612), [7b4a9ec](https://github.com/laravel/framework/commit/7b4a9ec6c58906eb73957015e4c78f73e780e944)) +- Added `is()` method to 1-1 relations for model comparison ([#34693](https://github.com/laravel/framework/pull/34693), [7ba2577](https://github.com/laravel/framework/commit/7ba257732d2342175a6ffe7db7a4ca847ca1d353)) +- Added `upsert()` to Eloquent and Base Query Builders ([#34698](https://github.com/laravel/framework/pull/34698), [#34712](https://github.com/laravel/framework/pull/34712), [58a0e1b](https://github.com/laravel/framework/commit/58a0e1b7e2bb6df3923883c4fc8cf13b1bce7322)) +- Support psql and pg_restore commands in schema load ([#34711](https://github.com/laravel/framework/pull/34711)) +- Added `Illuminate\Database\Schema\Builder::dropColumns()` method on the schema class ([#34720](https://github.com/laravel/framework/pull/34720)) +- Added `yearlyOn()` method to scheduler ([#34728](https://github.com/laravel/framework/pull/34728)) +- Added `restrictOnDelete()` method to ForeignKeyDefinition class ([#34752](https://github.com/laravel/framework/pull/34752)) +- Added `newLine()` method to `InteractsWithIO` trait ([#34754](https://github.com/laravel/framework/pull/34754)) +- Added `isNotEmpty()` method to HtmlString ([#34774](https://github.com/laravel/framework/pull/34774)) +- Added `delay()` to PendingChain ([#34789](https://github.com/laravel/framework/pull/34789)) +- Added "multiple_of" validation rule ([#34788](https://github.com/laravel/framework/pull/34788)) +- Added custom methods proxy support for jobs `dispatch()` ([#34781](https://github.com/laravel/framework/pull/34781)) +- Added `QueryBuilder::clone()` ([#34780](https://github.com/laravel/framework/pull/34780)) +- Support bus chain on fake ([a952ac24](https://github.com/laravel/framework/commit/a952ac24f34b832270a2f80cd425c2afe4c61fc1)) +- Added missing force flag to `queue:clear` command ([#34809](https://github.com/laravel/framework/pull/34809)) +- Added `dropConstrainedForeignId()` to `Blueprint ([#34806](https://github.com/laravel/framework/pull/34806)) +- Implement `supportsTags()` on the Cache Repository ([#34820](https://github.com/laravel/framework/pull/34820)) +- Added `canAny` to user model ([#34815](https://github.com/laravel/framework/pull/34815)) +- Added `when()` and `unless()` methods to MailMessage ([#34814](https://github.com/laravel/framework/pull/34814)) + +### Fixed +- Fixed collection wrapping in `BelongsToManyRelationship` ([9245807](https://github.com/laravel/framework/commit/9245807f8a1132a30ce669513cf0e99e9e078267)) +- Fixed `LengthAwarePaginator` translations issue ([#34714](https://github.com/laravel/framework/pull/34714)) + +### Changed +- Improve `schedule:work` command ([#34736](https://github.com/laravel/framework/pull/34736), [bbddba2](https://github.com/laravel/framework/commit/bbddba279bc781fc2868a6967430943de636614f)) +- Guard against invalid guard in `make:policy` ([#34792](https://github.com/laravel/framework/pull/34792)) +- Fixed router inconsistency for namespaced route groups ([#34793](https://github.com/laravel/framework/pull/34793)) + + +## [v8.9.0 (2020-10-06)](https://github.com/laravel/framework/compare/v8.8.0...v8.9.0) + +### Added +- Added support `times()` with `raw()` from `Illuminate\Database\Eloquent\Factories\Factory` ([#34667](https://github.com/laravel/framework/pull/34667)) +- Added `Illuminate\Pagination\AbstractPaginator::through()` ([#34657](https://github.com/laravel/framework/pull/34657)) +- Added `extendsFirst()` method similar to `includesFirst()` to view ([#34648](https://github.com/laravel/framework/pull/34648)) +- Allowed `Illuminate\Http\Client\PendingRequest::attach()` method to accept many files ([#34697](https://github.com/laravel/framework/pull/34697), [1bb7ad6](https://github.com/laravel/framework/commit/1bb7ad664a3607f719af2d91c3f95cf71662dcd2)) +- Allowed serializing custom casts when converting a model to an array ([#34702](https://github.com/laravel/framework/pull/34702)) + +### Fixed +- Added missed RESET_THROTTLED constant to Password Facade ([#34641](https://github.com/laravel/framework/pull/34641)) +- Fixed queue clearing when blocking ([#34659](https://github.com/laravel/framework/pull/34659)) +- Fixed missing import in TestView.php ([#34677](https://github.com/laravel/framework/pull/34677)) +- Use `getRealPath` to ensure console command class names are generated correctly in `Illuminate\Foundation\Console\Kernel` ([#34653](https://github.com/laravel/framework/pull/34653)) +- Added `pg_dump --no-owner` and `--no-acl` to avoid owner/permission issues in `Illuminate\Database\Schema\PostgresSchemaState::baseDumpCommand()` ([#34689](https://github.com/laravel/framework/pull/34689)) +- Fixed `queue:failed` command when Class not exists ([#34696](https://github.com/laravel/framework/pull/34696)) + +### Performance +- Increase performance of `Str::before()` by over 60% ([#34642](https://github.com/laravel/framework/pull/34642)) + + +## [v8.8.0 (2020-10-02)](https://github.com/laravel/framework/compare/v8.7.1...v8.8.0) + +### Added +- Proxy URL Generation in `VerifyEmail` ([#34572](https://github.com/laravel/framework/pull/34572)) +- Added `Illuminate\Collections\Traits\EnumeratesValues::pipeInto()` ([#34600](https://github.com/laravel/framework/pull/34600)) +- Added `Illuminate\Http\Client\PendingRequest::withUserAgent()` ([#34611](https://github.com/laravel/framework/pull/34611)) +- Added `schedule:work` command ([#34618](https://github.com/laravel/framework/pull/34618)) +- Added support for appendable (prepends) component attributes ([09b887b](https://github.com/laravel/framework/commit/09b887b85614d3e2539e74f40d7aa9c1c9f903d3), [53fbc9f](https://github.com/laravel/framework/commit/53fbc9f3768f611c960a5d891a1abb259163978a)) + +### Fixed +- Fixed `Illuminate\Http\Client\Response::throw()` ([#34597](https://github.com/laravel/framework/pull/34597)) +- Fixed breaking change in migrate command ([b2a3641](https://github.com/laravel/framework/commit/b2a36411a774dba218fa312b8fd3bcf4be44a4e5)) + +### Changed +- Changing the dump and restore method for a PostgreSQL database ([#34293](https://github.com/laravel/framework/pull/34293)) + + +## [v8.7.1 (2020-09-29)](https://github.com/laravel/framework/compare/v8.7.0...v8.7.1) + +### Fixed +- Remove type hints ([1b3f62a](https://github.com/laravel/framework/commit/1b3f62aaeced2c9761a6052a7f0d3c1a046851c9)) + + +## [v8.7.0 (2020-09-29)](https://github.com/laravel/framework/compare/v8.6.0...v8.7.0) + +### Added +- Added `tg://` protocol in "url" validation rule ([#34464](https://github.com/laravel/framework/pull/34464)) +- Allow dynamic factory methods to obey newFactory method on model ([#34492](https://github.com/laravel/framework/pull/34492), [4708e9e](https://github.com/laravel/framework/commit/4708e9ef8f7cde617a5820f07cfd350daaba0e0f)) +- Added `no-reload` option to `serve` command ([9cc2622](https://github.com/laravel/framework/commit/9cc2622a9122f5108a694856055c13db8a5f80dc)) +- Added `perHour()` and `perDay()` methods to `Illuminate\Cache\RateLimiting\Limit` ([#34530](https://github.com/laravel/framework/pull/34530)) +- Added `Illuminate\Http\Client\Response::onError()` ([#34558](https://github.com/laravel/framework/pull/34558), [d034e2c](https://github.com/laravel/framework/commit/d034e2c55c6502fa0c2bebb6cbf99c5e685beaa5)) +- Added `X-Message-ID` to `Mailgun` and `Ses Transport` ([#34567](https://github.com/laravel/framework/pull/34567)) + +### Fixed +- Fixed incompatibility with Lumen route function in `Illuminate\Session\Middleware\StartSession` ([#34491](https://github.com/laravel/framework/pull/34491)) +- Fixed: Eager loading MorphTo relationship does not honor each models `$keyType` ([#34531](https://github.com/laravel/framework/pull/34531), [c3f44c7](https://github.com/laravel/framework/commit/c3f44c712833d83061452e9a362a5e10fa424863)) +- Fixed translation label ("Pagination Navigation") for the Tailwind blade ([#34568](https://github.com/laravel/framework/pull/34568)) +- Fixed save keys on increment / decrement in Model ([77db028](https://github.com/laravel/framework/commit/77db028225ccd6ec6bc3359f69482f2e4cc95faf)) + +### Changed +- Allow modifiers in date format in Model ([#34507](https://github.com/laravel/framework/pull/34507)) +- Allow for dynamic calls of anonymous component with varied attributes ([#34498](https://github.com/laravel/framework/pull/34498)) +- Cast `Expression` as string so it can be encoded ([#34569](https://github.com/laravel/framework/pull/34569)) + + +## [v8.6.0 (2020-09-22)](https://github.com/laravel/framework/compare/v8.5.0...v8.6.0) + +### Added +- Added `Illuminate\Collections\LazyCollection::takeUntilTimeout()` ([0aabf24](https://github.com/laravel/framework/commit/0aabf2472850a9d573907ca092bf5e3cfe26fab3)) +- Added `--schema-path` option to `migrate:fresh` command ([#34419](https://github.com/laravel/framework/pull/34419)) + +### Fixed +- Fixed problems with dots in validator ([#34355](https://github.com/laravel/framework/pull/34355)) +- Maintenance mode: Fix empty Retry-After header ([#34412](https://github.com/laravel/framework/pull/34412)) +- Fixed bug with error handling in closure scheduled tasks ([#34420](https://github.com/laravel/framework/pull/34420)) +- Don't double escape on `ComponentTagCompiler.php` ([12ba0d9](https://github.com/laravel/framework/commit/12ba0d937d54e81eccf8f0a80150f0d70604e1c2)) +- Fixed `mysqldump: unknown variable 'column-statistics=0` for MariaDB schema dump ([#34442](https://github.com/laravel/framework/pull/34442)) + + +## [v8.5.0 (2020-09-19)](https://github.com/laravel/framework/compare/v8.4.0...v8.5.0) + +### Added +- Allow clearing an SQS queue by `queue:clear` command ([#34383](https://github.com/laravel/framework/pull/34383), [de811ea](https://github.com/laravel/framework/commit/de811ea7f7dc7ecfc686b25fba48e4b0dac473e6)) +- Added `Illuminate\Foundation\Auth\EmailVerificationRequest` ([4bde31b](https://github.com/laravel/framework/commit/4bde31b24bf01b4d4a35ad31fafd8e4ca203b0f2)) +- Auto handle `Jsonable` values passed to `castAsJson()` ([#34392](https://github.com/laravel/framework/pull/34392)) +- Added `crossJoinSub()` method to the query builder ([#34400](https://github.com/laravel/framework/pull/34400)) +- Added `Illuminate\Session\Store::passwordConfirmed()` ([fb3f45a](https://github.com/laravel/framework/commit/fb3f45aa0142764c5c29b97e8bcf8328091986e9)) + +### Changed +- Check for view existence first in `Illuminate\Mail\Markdown::render()` ([5f78c90](https://github.com/laravel/framework/commit/5f78c90a7af118dd07703a78da06586016973a66)) +- Guess the model name when using the `make:factory` command ([#34373](https://github.com/laravel/framework/pull/34373)) + + +## [v8.4.0 (2020-09-16)](https://github.com/laravel/framework/compare/v8.3.0...v8.4.0) + +### Added +- Added SQLite schema dump support ([#34323](https://github.com/laravel/framework/pull/34323)) +- Added `queue:clear` command ([#34330](https://github.com/laravel/framework/pull/34330), [06b378c](https://github.com/laravel/framework/commit/06b378c07b2ea989aa3e947ca003e96ea277153c)) + +### Fixed +- Fixed `minimal.blade.php` ([#34379](https://github.com/laravel/framework/pull/34379)) +- Don't double escape on ComponentTagCompiler.php ([ec75487](https://github.com/laravel/framework/commit/ec75487062506963dd27a4302fe3680c0e3681a3)) +- Fixed dots in attribute names in `DynamicComponent` ([2d1d962](https://github.com/laravel/framework/commit/2d1d96272a94bce123676ed742af2d80ba628ba4)) + +### Changed +- Show warning when view exists when using artisan `make:component` ([#34376](https://github.com/laravel/framework/pull/34376), [0ce75e0](https://github.com/laravel/framework/commit/0ce75e01a66ba4b13bbe4cbed85564f1dc76bb05)) +- Call the booting/booted callbacks from the container ([#34370](https://github.com/laravel/framework/pull/34370)) ## [v8.3.0 (2020-09-15)](https://github.com/laravel/framework/compare/v8.2.0...v8.3.0) diff --git a/README.md b/README.md index ef4bc184428e..6e9702b3a98c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@

Build Status -Total Downloads -Latest Stable Version -License +Total Downloads +Latest Stable Version +License

## About Laravel diff --git a/composer.json b/composer.json index 1868017be6a5..581d0ead203b 100644 --- a/composer.json +++ b/composer.json @@ -15,31 +15,31 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", "doctrine/inflector": "^1.4|^2.0", - "dragonmantank/cron-expression": "^3.0", + "dragonmantank/cron-expression": "^3.0.2", "egulias/email-validator": "^2.1.10", "league/commonmark": "^1.3", - "league/flysystem": "^1.0.34", + "league/flysystem": "^1.1", "monolog/monolog": "^2.0", - "nesbot/carbon": "^2.17", - "opis/closure": "^3.5.3", + "nesbot/carbon": "^2.31", + "opis/closure": "^3.6", "psr/container": "^1.0", "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" @@ -78,17 +78,17 @@ "illuminate/view": "self.version" }, "require-dev": { - "aws/aws-sdk-php": "^3.0", - "doctrine/dbal": "^2.6", - "filp/whoops": "^2.4", + "aws/aws-sdk-php": "^3.155", + "doctrine/dbal": "^2.6|^3.0", + "filp/whoops": "^2.8", "guzzlehttp/guzzle": "^6.5.5|^7.0.1", "league/flysystem-cached-adapter": "^1.0", - "mockery/mockery": "^1.3.1", - "orchestra/testbench-core": "^6.0", + "mockery/mockery": "^1.4.2", + "orchestra/testbench-core": "^6.8", "pda/pheanstalk": "^4.0", - "phpunit/phpunit": "^8.4|^9.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" @@ -128,24 +128,24 @@ "ext-pcntl": "Required to use all features of the queue worker.", "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", - "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).", - "filp/whoops": "Required for friendly error pages in development (^2.4).", - "fzaninotto/faker": "Required to use the eloquent factory builder (^1.9.1).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).", + "filp/whoops": "Required for friendly error pages in development (^2.8).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "guzzlehttp/guzzle": "Required to use the HTTP Client, Mailgun mail driver and the ping methods on schedules (^6.5.5|^7.0.1).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", - "mockery/mockery": "Required to use mocking (^1.3.1).", + "mockery/mockery": "Required to use mocking (^1.4.2).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", - "phpunit/phpunit": "Required to use assertions and run tests (^8.4|^9.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^8.5.8|^9.3.3).", "predis/predis": "Required to use the predis connector (^1.1.2).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^5.1).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^5.1).", + "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/Middleware/EnsureEmailIsVerified.php b/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php index 1f73e576ad63..8f2b33ae5c72 100644 --- a/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php +++ b/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\URL; class EnsureEmailIsVerified { @@ -14,7 +15,7 @@ class EnsureEmailIsVerified * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string|null $redirectToRoute - * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse|null */ public function handle($request, Closure $next, $redirectToRoute = null) { @@ -23,7 +24,7 @@ public function handle($request, Closure $next, $redirectToRoute = null) ! $request->user()->hasVerifiedEmail())) { return $request->expectsJson() ? abort(403, 'Your email address is not verified.') - : Redirect::route($redirectToRoute ?: 'verification.notice'); + : Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice')); } return $next($request); diff --git a/src/Illuminate/Auth/Notifications/ResetPassword.php b/src/Illuminate/Auth/Notifications/ResetPassword.php index 05d0104360a2..00042d19c04d 100644 --- a/src/Illuminate/Auth/Notifications/ResetPassword.php +++ b/src/Illuminate/Auth/Notifications/ResetPassword.php @@ -72,6 +72,17 @@ public function toMail($notifiable) ], false)); } + return $this->buildMailMessage($url); + } + + /** + * Get the reset password notification mail message for the given URL. + * + * @param string $url + * @return \Illuminate\Notifications\Messages\MailMessage + */ + protected function buildMailMessage($url) + { return (new MailMessage) ->subject(Lang::get('Reset Password Notification')) ->line(Lang::get('You are receiving this email because we received a password reset request for your account.')) diff --git a/src/Illuminate/Auth/Notifications/VerifyEmail.php b/src/Illuminate/Auth/Notifications/VerifyEmail.php index f746685fc44a..7a5cf916449d 100644 --- a/src/Illuminate/Auth/Notifications/VerifyEmail.php +++ b/src/Illuminate/Auth/Notifications/VerifyEmail.php @@ -11,6 +11,13 @@ class VerifyEmail extends Notification { + /** + * The callback that should be used to create the verify email URL. + * + * @var \Closure|null + */ + public static $createUrlCallback; + /** * The callback that should be used to build the mail message. * @@ -43,10 +50,21 @@ public function toMail($notifiable) return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl); } + return $this->buildMailMessage($verificationUrl); + } + + /** + * Get the verify email notification mail message for the given URL. + * + * @param string $url + * @return \Illuminate\Notifications\Messages\MailMessage + */ + protected function buildMailMessage($url) + { return (new MailMessage) ->subject(Lang::get('Verify Email Address')) ->line(Lang::get('Please click the button below to verify your email address.')) - ->action(Lang::get('Verify Email Address'), $verificationUrl) + ->action(Lang::get('Verify Email Address'), $url) ->line(Lang::get('If you did not create an account, no further action is required.')); } @@ -58,6 +76,10 @@ public function toMail($notifiable) */ protected function verificationUrl($notifiable) { + if (static::$createUrlCallback) { + return call_user_func(static::$createUrlCallback, $notifiable); + } + return URL::temporarySignedRoute( 'verification.verify', Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), @@ -68,6 +90,17 @@ protected function verificationUrl($notifiable) ); } + /** + * Set a callback that should be used when creating the email verification URL. + * + * @param \Closure $callback + * @return void + */ + public static function createUrlUsing($callback) + { + static::$createUrlCallback = $callback; + } + /** * Set a callback that should be used when building the notification mail message. * 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/Auth/composer.json b/src/Illuminate/Auth/composer.json index 8c994018164c..842066cdef12 100644 --- a/src/Illuminate/Auth/composer.json +++ b/src/Illuminate/Auth/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", "illuminate/http": "^8.0", diff --git a/src/Illuminate/Broadcasting/BroadcastEvent.php b/src/Illuminate/Broadcasting/BroadcastEvent.php index 775df78059d7..e9c0897a553b 100644 --- a/src/Illuminate/Broadcasting/BroadcastEvent.php +++ b/src/Illuminate/Broadcasting/BroadcastEvent.php @@ -46,6 +46,7 @@ public function __construct($event) $this->event = $event; $this->tries = property_exists($event, 'tries') ? $event->tries : null; $this->timeout = property_exists($event, 'timeout') ? $event->timeout : null; + $this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null; } /** diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index e3bdb03aaca1..e5ec7346e813 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -2,7 +2,9 @@ namespace Illuminate\Broadcasting; +use Ably\AblyRest; use Closure; +use Illuminate\Broadcasting\Broadcasters\AblyBroadcaster; use Illuminate\Broadcasting\Broadcasters\LogBroadcaster; use Illuminate\Broadcasting\Broadcasters\NullBroadcaster; use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster; @@ -70,7 +72,7 @@ public function routes(array $attributes = null) $router->match( ['get', 'post'], '/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate' - ); + )->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]); }); } @@ -220,6 +222,17 @@ protected function createPusherDriver(array $config) return new PusherBroadcaster($pusher); } + /** + * Create an instance of the driver. + * + * @param array $config + * @return \Illuminate\Contracts\Broadcasting\Broadcaster + */ + protected function createAblyDriver(array $config) + { + return new AblyBroadcaster(new AblyRest($config)); + } + /** * Create an instance of the driver. * diff --git a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php new file mode 100644 index 000000000000..7857c26dce3f --- /dev/null +++ b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php @@ -0,0 +1,200 @@ +ably = $ably; + } + + /** + * Authenticate the incoming request for a given channel. + * + * @param \Illuminate\Http\Request $request + * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function auth($request) + { + $channelName = $this->normalizeChannelName($request->channel_name); + + if (empty($request->channel_name) || + ($this->isGuardedChannel($request->channel_name) && + ! $this->retrieveUser($request, $channelName))) { + throw new AccessDeniedHttpException; + } + + return parent::verifyUserCanAccessChannel( + $request, $channelName + ); + } + + /** + * Return the valid authentication response. + * + * @param \Illuminate\Http\Request $request + * @param mixed $result + * @return mixed + */ + public function validAuthenticationResponse($request, $result) + { + if (Str::startsWith($request->channel_name, 'private')) { + $signature = $this->generateAblySignature( + $request->channel_name, $request->socket_id + ); + + return ['auth' => $this->getPublicToken().':'.$signature]; + } + + $channelName = $this->normalizeChannelName($request->channel_name); + + $signature = $this->generateAblySignature( + $request->channel_name, + $request->socket_id, + $userData = array_filter([ + 'user_id' => $this->retrieveUser($request, $channelName)->getAuthIdentifier(), + 'user_info' => $result, + ]) + ); + + return [ + 'auth' => $this->getPublicToken().':'.$signature, + 'channel_data' => json_encode($userData), + ]; + } + + /** + * Generate the signature needed for Ably authentication headers. + * + * @param string $channelName + * @param string $socketId + * @param array|null $userData + * @return string + */ + public function generateAblySignature($channelName, $socketId, $userData = null) + { + return hash_hmac( + 'sha256', + sprintf('%s:%s%s', $socketId, $channelName, $userData ? ':'.json_encode($userData) : ''), + $this->getPrivateToken(), + ); + } + + /** + * Broadcast the given event. + * + * @param array $channels + * @param string $event + * @param array $payload + * @return void + */ + public function broadcast(array $channels, $event, array $payload = []) + { + foreach ($this->formatChannels($channels) as $channel) { + $this->ably->channels->get($channel)->publish($event, $payload); + } + } + + /** + * Return true if channel is protected by authentication. + * + * @param string $channel + * @return bool + */ + public function isGuardedChannel($channel) + { + return Str::startsWith($channel, ['private-', 'presence-']); + } + + /** + * Remove prefix from channel name. + * + * @param string $channel + * @return string + */ + public function normalizeChannelName($channel) + { + if ($this->isGuardedChannel($channel)) { + return Str::startsWith($channel, 'private-') + ? Str::replaceFirst('private-', '', $channel) + : Str::replaceFirst('presence-', '', $channel); + } + + return $channel; + } + + /** + * Format the channel array into an array of strings. + * + * @param array $channels + * @return array + */ + protected function formatChannels(array $channels) + { + return array_map(function ($channel) { + $channel = (string) $channel; + + if (Str::startsWith($channel, ['private-', 'presence-'])) { + return Str::startsWith($channel, 'private-') + ? Str::replaceFirst('private-', 'private:', $channel) + : Str::replaceFirst('presence-', 'presence:', $channel); + } + + return 'public:'.$channel; + }, $channels); + } + + /** + * Get the public token value from the Ably key. + * + * @return mixed + */ + protected function getPublicToken() + { + return Str::before($this->ably->options->key, ':'); + } + + /** + * Get the private token value from the Ably key. + * + * @return mixed + */ + protected function getPrivateToken() + { + return Str::after($this->ably->options->key, ':'); + } + + /** + * Get the underlying Ably SDK instance. + * + * @return \Ably\AblyRest + */ + public function getAbly() + { + return $this->ably; + } +} diff --git a/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php index d39258ff5b51..e48b15195741 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php @@ -317,7 +317,7 @@ protected function retrieveChannelOptions($channel) } /** - * Check if channel name from request match a pattern from registered channels. + * Check if the channel name from the request matches a pattern from registered channels. * * @param string $channel * @param string $pattern diff --git a/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php index 68daf9da4b26..f55cf02b86d4 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php @@ -42,8 +42,9 @@ public function auth($request) { $channelName = $this->normalizeChannelName($request->channel_name); - if ($this->isGuardedChannel($request->channel_name) && - ! $this->retrieveUser($request, $channelName)) { + if (empty($request->channel_name) || + ($this->isGuardedChannel($request->channel_name) && + ! $this->retrieveUser($request, $channelName))) { throw new AccessDeniedHttpException; } @@ -120,7 +121,7 @@ public function broadcast(array $channels, $event, array $payload = []) throw new BroadcastException( ! empty($response['body']) - ? sprintf('Pusher error: %s.', $response['status']) + ? sprintf('Pusher error: %s.', $response['body']) : 'Failed to connect to Pusher.' ); } diff --git a/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php index 18cb0fef3cdb..2fea183fb6a0 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php @@ -60,8 +60,9 @@ public function auth($request) str_replace($this->prefix, '', $request->channel_name) ); - if ($this->isGuardedChannel($request->channel_name) && - ! $this->retrieveUser($request, $channelName)) { + if (empty($request->channel_name) || + ($this->isGuardedChannel($request->channel_name) && + ! $this->retrieveUser($request, $channelName))) { throw new AccessDeniedHttpException; } diff --git a/src/Illuminate/Broadcasting/composer.json b/src/Illuminate/Broadcasting/composer.json index 13fd662f1d81..3a7fe618a450 100644 --- a/src/Illuminate/Broadcasting/composer.json +++ b/src/Illuminate/Broadcasting/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "psr/log": "^1.0", "illuminate/bus": "^8.0", diff --git a/src/Illuminate/Bus/Batch.php b/src/Illuminate/Bus/Batch.php index 687fffc25dfc..cac16e1e9f51 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -81,21 +81,21 @@ class Batch implements Arrayable, JsonSerializable /** * The date indicating when the batch was created. * - * @var \Illuminate\Support\CarbonImmutable + * @var \Carbon\CarbonImmutable */ public $createdAt; /** * The date indicating when the batch was cancelled. * - * @var \Illuminate\Support\CarbonImmutable|null + * @var \Carbon\CarbonImmutable|null */ public $cancelledAt; /** * The date indicating when the batch was finished. * - * @var \Illuminate\Support\CarbonImmutable|null + * @var \Carbon\CarbonImmutable|null */ public $finishedAt; @@ -111,9 +111,9 @@ class Batch implements Arrayable, JsonSerializable * @param int $failedJobs * @param array $failedJobIds * @param array $options - * @param \Illuminate\Support\CarbonImmutable $createdAt - * @param \Illuminate\Support\CarbonImmutable|null $cancelledAt - * @param \Illuminate\Support\CarbonImmutable|null $finishedAt + * @param \Carbon\CarbonImmutable $createdAt + * @param \Carbon\CarbonImmutable|null $cancelledAt + * @param \Carbon\CarbonImmutable|null $finishedAt * @return void */ public function __construct(QueueFactory $queue, @@ -161,18 +161,31 @@ public function fresh() */ public function add($jobs) { - $jobs = Collection::wrap($jobs)->map(function ($job) { - if ($job instanceof Closure) { - $job = CallQueuedClosure::create($job); - } + $count = 0; + + $jobs = Collection::wrap($jobs)->map(function ($job) use (&$count) { + $job = $job instanceof Closure ? CallQueuedClosure::create($job) : $job; + + if (is_array($job)) { + $count += count($job); + + return with($this->prepareBatchedChain($job), function ($chain) { + return $chain->first() + ->allOnQueue($this->options['queue'] ?? null) + ->allOnConnection($this->options['connection'] ?? null) + ->chain($chain->slice(1)->values()->all()); + }); + } else { + $job->withBatchId($this->id); - $job->withBatchId($this->id); + $count++; + } return $job; }); - $this->repository->transaction(function () use ($jobs) { - $this->repository->incrementTotalJobs($this->id, count($jobs)); + $this->repository->transaction(function () use ($jobs, $count) { + $this->repository->incrementTotalJobs($this->id, $count); $this->queue->connection($this->options['connection'] ?? null)->bulk( $jobs->all(), @@ -184,6 +197,21 @@ public function add($jobs) return $this->fresh(); } + /** + * Prepare a chain that exists within the jobs being added. + * + * @param array $chain + * @return \Illuminate\Support\Collection + */ + protected function prepareBatchedChain(array $chain) + { + return collect($chain)->map(function ($job) { + $job = $job instanceof Closure ? CallQueuedClosure::create($job) : $job; + + return $job->withBatchId($this->id); + }); + } + /** * Get the total number of jobs that have been processed by the batch thus far. * @@ -239,7 +267,7 @@ public function recordSuccessfulJob(string $jobId) * Decrement the pending jobs for the batch. * * @param string $jobId - * @return int + * @return \Illuminate\Bus\UpdatedBatchJobCounts */ public function decrementPendingJobs(string $jobId) { @@ -322,7 +350,7 @@ public function recordFailedJob(string $jobId, $e) * Increment the failed jobs for the batch. * * @param string $jobId - * @return int + * @return \Illuminate\Bus\UpdatedBatchJobCounts */ public function incrementFailedJobs(string $jobId) { @@ -393,7 +421,7 @@ public function delete() * Invoke a batch callback handler. * * @param \Illuminate\Queue\SerializableClosure|callable $handler - * @param \Illuminate\Bus $batch + * @param \Illuminate\Bus\Batch $batch * @param \Throwable|null $e * @return void */ diff --git a/src/Illuminate/Bus/BatchFactory.php b/src/Illuminate/Bus/BatchFactory.php index 4b46ff0985a6..2c3a4e96ce57 100644 --- a/src/Illuminate/Bus/BatchFactory.php +++ b/src/Illuminate/Bus/BatchFactory.php @@ -36,9 +36,9 @@ public function __construct(QueueFactory $queue) * @param int $failedJobs * @param array $failedJobIds * @param array $options - * @param \Illuminate\Support\CarbonImmutable $createdAt - * @param \Illuminate\Support\CarbonImmutable|null $cancelledAt - * @param \Illuminate\Support\CarbonImmutable|null $finishedAt + * @param \Carbon\CarbonImmutable $createdAt + * @param \Carbon\CarbonImmutable|null $cancelledAt + * @param \Carbon\CarbonImmutable|null $finishedAt * @return \Illuminate\Bus\Batch */ public function make(BatchRepository $repository, diff --git a/src/Illuminate/Bus/BusServiceProvider.php b/src/Illuminate/Bus/BusServiceProvider.php index bc1fc7b26e72..6f3da09d10b4 100644 --- a/src/Illuminate/Bus/BusServiceProvider.php +++ b/src/Illuminate/Bus/BusServiceProvider.php @@ -47,7 +47,7 @@ protected function registerBatchServices() return new DatabaseBatchRepository( $app->make(BatchFactory::class), $app->make('db')->connection(config('queue.batching.database')), - config('queue.batching.table', 'job_batches'), + config('queue.batching.table', 'job_batches') ); }); } 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/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index dbbab31b3ea1..b33747b079d1 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -146,7 +146,7 @@ public function findBatch(string $batchId) /** * Create a new batch of queueable jobs. * - * @param \Illuminate\Support\Collection|array $jobs + * @param \Illuminate\Support\Collection|array|mixed $jobs * @return \Illuminate\Bus\PendingBatch */ public function batch($jobs) diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index 9db31e76a188..7eab4e36112f 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -215,6 +215,8 @@ public function queue() * Dispatch the batch. * * @return \Illuminate\Bus\Batch + * + * @throws \Throwable */ public function dispatch() { diff --git a/src/Illuminate/Bus/PrunableBatchRepository.php b/src/Illuminate/Bus/PrunableBatchRepository.php new file mode 100644 index 000000000000..3f972553b597 --- /dev/null +++ b/src/Illuminate/Bus/PrunableBatchRepository.php @@ -0,0 +1,16 @@ +afterCommit = true; + + return $this; + } + + /** + * Indicate that the job should not wait until database transactions have been committed before dispatching. + * + * @return $this + */ + public function beforeCommit() + { + $this->afterCommit = false; + + return $this; + } + /** * Specify the middleware the job should be dispatched through. * diff --git a/src/Illuminate/Bus/composer.json b/src/Illuminate/Bus/composer.json index 68620dc5a10c..12713a61c3bd 100644 --- a/src/Illuminate/Bus/composer.json +++ b/src/Illuminate/Bus/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", "illuminate/pipeline": "^8.0", diff --git a/src/Illuminate/Cache/CacheLock.php b/src/Illuminate/Cache/CacheLock.php new file mode 100644 index 000000000000..310d9fb5d35c --- /dev/null +++ b/src/Illuminate/Cache/CacheLock.php @@ -0,0 +1,85 @@ +store = $store; + } + + /** + * Attempt to acquire the lock. + * + * @return bool + */ + public function acquire() + { + if (method_exists($this->store, 'add') && $this->seconds > 0) { + return $this->store->add( + $this->name, $this->owner, $this->seconds + ); + } + + if (! is_null($this->store->get($this->name))) { + return false; + } + + return ($this->seconds > 0) + ? $this->store->put($this->name, $this->owner, $this->seconds) + : $this->store->forever($this->name, $this->owner, $this->seconds); + } + + /** + * Release the lock. + * + * @return bool + */ + public function release() + { + if ($this->isOwnedByCurrentProcess()) { + return $this->store->forget($this->name); + } + + return false; + } + + /** + * Releases this lock regardless of ownership. + * + * @return void + */ + public function forceRelease() + { + $this->store->forget($this->name); + } + + /** + * Returns the owner value written into the driver for this lock. + * + * @return mixed + */ + protected function getCurrentOwner() + { + return $this->store->get($this->name); + } +} diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index f100d11efad6..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) + )); } /** @@ -314,7 +320,11 @@ protected function getPrefix(array $config) */ protected function getConfig($name) { - return $this->app['config']["cache.stores.{$name}"]; + if (! is_null($name) && $name !== 'null') { + return $this->app['config']["cache.stores.{$name}"]; + } + + return ['driver' => '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 c868b145eed4..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. * @@ -155,8 +162,6 @@ public function add($key, $value, $seconds) 'expiration' => $expiration, ]) >= 1; } - - return false; } /** @@ -267,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, @@ -333,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/FileStore.php b/src/Illuminate/Cache/FileStore.php index 7295d9e6d205..c3d0bc9c34c8 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -3,13 +3,16 @@ namespace Illuminate\Cache; use Exception; +use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Store; +use Illuminate\Contracts\Filesystem\LockTimeoutException; use Illuminate\Filesystem\Filesystem; +use Illuminate\Filesystem\LockableFile; use Illuminate\Support\InteractsWithTime; -class FileStore implements Store +class FileStore implements Store, LockProvider { - use InteractsWithTime, RetrievesMultipleKeys; + use InteractsWithTime, HasCacheLock, RetrievesMultipleKeys; /** * The Illuminate Filesystem instance. @@ -83,6 +86,45 @@ public function put($key, $value, $seconds) return false; } + /** + * Store an item in the cache if the key doesn't exist. + * + * @param string $key + * @param mixed $value + * @param int $seconds + * @return bool + */ + public function add($key, $value, $seconds) + { + $this->ensureCacheDirectoryExists($path = $this->path($key)); + + $file = new LockableFile($path, 'c+'); + + try { + $file->getExclusiveLock(); + } catch (LockTimeoutException $e) { + $file->close(); + + return false; + } + + $expire = $file->read(10); + + if (empty($expire) || $this->currentTime() >= $expire) { + $file->truncate() + ->write($this->expiration($seconds).serialize($value)) + ->close(); + + $this->ensureFileHasCorrectPermissions($path); + + return true; + } + + $file->close(); + + return false; + } + /** * Create the file cache directory if necessary. * diff --git a/src/Illuminate/Cache/HasCacheLock.php b/src/Illuminate/Cache/HasCacheLock.php new file mode 100644 index 000000000000..82ad9c2b31f5 --- /dev/null +++ b/src/Illuminate/Cache/HasCacheLock.php @@ -0,0 +1,31 @@ +lock($name, 0, $owner); + } +} diff --git a/src/Illuminate/Cache/NoLock.php b/src/Illuminate/Cache/NoLock.php new file mode 100644 index 000000000000..68560f8f83d3 --- /dev/null +++ b/src/Illuminate/Cache/NoLock.php @@ -0,0 +1,46 @@ +owner; + } +} diff --git a/src/Illuminate/Cache/NullStore.php b/src/Illuminate/Cache/NullStore.php index 43231b492347..0fe4268f7a3c 100755 --- a/src/Illuminate/Cache/NullStore.php +++ b/src/Illuminate/Cache/NullStore.php @@ -2,7 +2,9 @@ namespace Illuminate\Cache; -class NullStore extends TaggableStore +use Illuminate\Contracts\Cache\LockProvider; + +class NullStore extends TaggableStore implements LockProvider { use RetrievesMultipleKeys; @@ -66,6 +68,31 @@ public function forever($key, $value) return false; } + /** + * Get a lock instance. + * + * @param string $name + * @param int $seconds + * @param string|null $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function lock($name, $seconds = 0, $owner = null) + { + return new NoLock($name, $seconds, $owner); + } + + /** + * Restore a lock instance using the owner identifier. + * + * @param string $name + * @param string $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function restoreLock($name, $owner) + { + return $this->lock($name, 0, $owner); + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Cache/RateLimiting/Limit.php b/src/Illuminate/Cache/RateLimiting/Limit.php index 2eee661b25ce..ab3463f51830 100644 --- a/src/Illuminate/Cache/RateLimiting/Limit.php +++ b/src/Illuminate/Cache/RateLimiting/Limit.php @@ -58,6 +58,30 @@ public static function perMinute($maxAttempts) return new static('', $maxAttempts); } + /** + * Create a new rate limit using hours as decay time. + * + * @param int $maxAttempts + * @param int $decayHours + * @return static + */ + public static function perHour($maxAttempts, $decayHours = 1) + { + return new static('', $maxAttempts, 60 * $decayHours); + } + + /** + * Create a new rate limit using days as decay time. + * + * @param int $maxAttempts + * @param int $decayDays + * @return static + */ + public static function perDay($maxAttempts, $decayDays = 1) + { + return new static('', $maxAttempts, 60 * 24 * $decayDays); + } + /** * Create a new unlimited rate limit. * 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/RedisTaggedCache.php b/src/Illuminate/Cache/RedisTaggedCache.php index 208ae94661be..ad50ce9adb41 100644 --- a/src/Illuminate/Cache/RedisTaggedCache.php +++ b/src/Illuminate/Cache/RedisTaggedCache.php @@ -179,7 +179,7 @@ protected function deleteValues($referenceKey) if (count($values) > 0) { foreach (array_chunk($values, 1000) as $valuesChunk) { - call_user_func_array([$this->store->connection(), 'del'], $valuesChunk); + $this->store->connection()->del(...$valuesChunk); } } } diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index a242e8afc346..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; @@ -477,7 +479,7 @@ public function clear() */ public function tags($names) { - if (! method_exists($this->store, 'tags')) { + if (! $this->supportsTags()) { throw new BadMethodCallException('This cache store does not support tagging.'); } @@ -501,6 +503,33 @@ protected function itemKey($key) return $key; } + /** + * Calculate the number of seconds for the given TTL. + * + * @param \DateTimeInterface|\DateInterval|int $ttl + * @return int + */ + protected function getSeconds($ttl) + { + $duration = $this->parseDateInterval($ttl); + + if ($duration instanceof DateTimeInterface) { + $duration = Carbon::now()->diffInRealSeconds($duration, false); + } + + return (int) $duration > 0 ? $duration : 0; + } + + /** + * Determine if the current store supports tags. + * + * @return bool + */ + public function supportsTags() + { + return method_exists($this->store, 'tags'); + } + /** * Get the default cache time. * @@ -613,23 +642,6 @@ public function offsetUnset($key) $this->forget($key); } - /** - * Calculate the number of seconds for the given TTL. - * - * @param \DateTimeInterface|\DateInterval|int $ttl - * @return int - */ - protected function getSeconds($ttl) - { - $duration = $this->parseDateInterval($ttl); - - if ($duration instanceof DateTimeInterface) { - $duration = Carbon::now()->diffInRealSeconds($duration, false); - } - - return (int) $duration > 0 ? $duration : 0; - } - /** * Handle dynamic calls into macros or pass missing methods to the store. * diff --git a/src/Illuminate/Cache/composer.json b/src/Illuminate/Cache/composer.json index 746d582569fd..b9ef4881b062 100755 --- a/src/Illuminate/Cache/composer.json +++ b/src/Illuminate/Cache/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", @@ -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/Arr.php b/src/Illuminate/Collections/Arr.php index fe40f398a5f1..9d08ae2c8c24 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -409,7 +409,7 @@ public static function only($array, $keys) * Pluck an array of values from an array. * * @param iterable $array - * @param string|array $value + * @param string|array|int|null $value * @param string|array|null $key * @return array */ @@ -604,7 +604,7 @@ public static function shuffle($array, $seed = null) * Sort the array using the given callback or "dot" notation. * * @param array $array - * @param callable|string|null $callback + * @param callable|array|string|null $callback * @return array */ public static function sort($array, $callback = null) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 5edee040f47e..6a8ab88818aa 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -608,7 +608,7 @@ public function last(callable $callback = null, $default = null) /** * Get the values of a given key. * - * @param string|array $value + * @param string|array|int|null $value * @param string|null $key * @return static */ @@ -884,6 +884,24 @@ public function reduce(callable $callback, $initial = null) return array_reduce($this->items, $callback, $initial); } + /** + * 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->items as $key => $value) { + $result = $callback($result, $value, $key); + } + + return $result; + } + /** * Replace the collection items with the given items. * @@ -1041,6 +1059,17 @@ public function split($numberOfGroups) return $groups; } + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return static + */ + public function splitIn($numberOfGroups) + { + return $this->chunk(ceil($this->count() / $numberOfGroups)); + } + /** * Chunk the collection into chunks of the given size. * @@ -1110,13 +1139,17 @@ public function sortDesc($options = SORT_REGULAR) /** * Sort the collection using the given callback. * - * @param callable|string $callback + * @param callable|array|string $callback * @param int $options * @param bool $descending * @return static */ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) { + if (is_array($callback) && ! is_callable($callback)) { + return $this->sortByMany($callback); + } + $results = []; $callback = $this->valueRetriever($callback); @@ -1141,6 +1174,50 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) return new static($results); } + /** + * Sort the collection using multiple comparisons. + * + * @param array $comparisons + * @return static + */ + protected function sortByMany(array $comparisons = []) + { + $items = $this->items; + + usort($items, function ($a, $b) use ($comparisons) { + foreach ($comparisons as $comparison) { + $comparison = Arr::wrap($comparison); + + $prop = $comparison[0]; + + $ascending = Arr::get($comparison, 1, true) === true || + Arr::get($comparison, 1, true) === 'asc'; + + $result = 0; + + if (is_callable($prop)) { + $result = $prop($a, $b); + } else { + $values = [Arr::get($a, $prop), Arr::get($b, $prop)]; + + if (! $ascending) { + $values = array_reverse($values); + } + + $result = $values[0] <=> $values[1]; + } + + if ($result === 0) { + continue; + } + + return $result; + } + }); + + return new static($items); + } + /** * Sort the collection in descending order using the given callback. * @@ -1276,7 +1353,7 @@ public function zip($items) return new static(func_get_args()); }, $this->items], $arrayableItems); - return new static(call_user_func_array('array_map', $params)); + return new static(array_map(...$params)); } /** diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 55b8d97d8540..ca51626b071e 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -845,6 +845,24 @@ public function reduce(callable $callback, $initial = null) 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; + } + /** * Replace the collection items with the given items. * @@ -1057,6 +1075,17 @@ public function chunk($size) }); } + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return static + */ + public function splitIn($numberOfGroups) + { + return $this->chunk(ceil($this->count() / $numberOfGroups)); + } + /** * Chunk the collection into chunks with a callback. * diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index 800c24613551..3355a9b2da29 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -35,7 +35,11 @@ * @property-read HigherOrderCollectionProxy $some * @property-read HigherOrderCollectionProxy $sortBy * @property-read HigherOrderCollectionProxy $sortByDesc + * @property-read HigherOrderCollectionProxy $skipUntil + * @property-read HigherOrderCollectionProxy $skipWhile * @property-read HigherOrderCollectionProxy $sum + * @property-read HigherOrderCollectionProxy $takeUntil + * @property-read HigherOrderCollectionProxy $takeWhile * @property-read HigherOrderCollectionProxy $unique * @property-read HigherOrderCollectionProxy $until */ @@ -44,7 +48,7 @@ trait EnumeratesValues /** * The methods that can be proxied. * - * @var array + * @var string[] */ protected static $proxies = [ 'average', @@ -197,7 +201,7 @@ public function containsStrict($key, $value = null) */ public function dd(...$args) { - call_user_func_array([$this, 'dump'], $args); + $this->dump(...$args); exit(1); } @@ -688,6 +692,17 @@ public function pipe(callable $callback) return $callback($this); } + /** + * Pass the collection into a new class. + * + * @param string $class + * @return mixed + */ + public function pipeInto($class) + { + return new $class($this); + } + /** * Pass the collection to the given callback and then return it. * diff --git a/src/Illuminate/Collections/composer.json b/src/Illuminate/Collections/composer.json index 2b0eaebf48f2..cb23d3e49486 100644 --- a/src/Illuminate/Collections/composer.json +++ b/src/Illuminate/Collections/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0" }, @@ -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/Config/composer.json b/src/Illuminate/Config/composer.json index 69cb909a0376..9d577bb46fae 100755 --- a/src/Illuminate/Config/composer.json +++ b/src/Illuminate/Config/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0" }, diff --git a/src/Illuminate/Console/Concerns/CallsCommands.php b/src/Illuminate/Console/Concerns/CallsCommands.php index e060c5562606..7e69b9b7891d 100644 --- a/src/Illuminate/Console/Concerns/CallsCommands.php +++ b/src/Illuminate/Console/Concerns/CallsCommands.php @@ -29,7 +29,7 @@ public function call($command, array $arguments = []) } /** - * Call another console command silently. + * Call another console command without output. * * @param \Symfony\Component\Console\Command\Command|string $command * @param array $arguments @@ -40,6 +40,18 @@ public function callSilent($command, array $arguments = []) return $this->runCommand($command, $arguments, new NullOutput); } + /** + * Call another console command without output. + * + * @param \Symfony\Component\Console\Command\Command|string $command + * @param array $arguments + * @return int + */ + public function callSilently($command, array $arguments = []) + { + return $this->callSilent($command, $arguments); + } + /** * Run the given the console command. * diff --git a/src/Illuminate/Console/Concerns/HasParameters.php b/src/Illuminate/Console/Concerns/HasParameters.php index 3f6f9c7642cf..e860ec2a2ec5 100644 --- a/src/Illuminate/Console/Concerns/HasParameters.php +++ b/src/Illuminate/Console/Concerns/HasParameters.php @@ -21,7 +21,7 @@ protected function specifyParameters() if ($arguments instanceof InputArgument) { $this->getDefinition()->addArgument($arguments); } else { - call_user_func_array([$this, 'addArgument'], $arguments); + $this->addArgument(...array_values($arguments)); } } @@ -29,7 +29,7 @@ protected function specifyParameters() if ($options instanceof InputOption) { $this->getDefinition()->addOption($options); } else { - call_user_func_array([$this, 'addOption'], $options); + $this->addOption(...array_values($options)); } } } diff --git a/src/Illuminate/Console/Concerns/InteractsWithIO.php b/src/Illuminate/Console/Concerns/InteractsWithIO.php index e61ef560420c..69d295c1efb1 100644 --- a/src/Illuminate/Console/Concerns/InteractsWithIO.php +++ b/src/Illuminate/Console/Concerns/InteractsWithIO.php @@ -2,6 +2,7 @@ namespace Illuminate\Console\Concerns; +use Closure; use Illuminate\Console\OutputStyle; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Str; @@ -237,6 +238,38 @@ public function table($headers, $rows, $tableStyle = 'default', array $columnSty $table->render(); } + /** + * Execute a given callback while advancing a progress bar. + * + * @param iterable|int $totalSteps + * @param \Closure $callback + * @return mixed|void + */ + public function withProgressBar($totalSteps, Closure $callback) + { + $bar = $this->output->createProgressBar( + is_iterable($totalSteps) ? count($totalSteps) : $totalSteps + ); + + $bar->start(); + + if (is_iterable($totalSteps)) { + foreach ($totalSteps as $value) { + $callback($value, $bar); + + $bar->advance(); + } + } else { + $callback($bar); + } + + $bar->finish(); + + if (is_iterable($totalSteps)) { + return $totalSteps; + } + } + /** * Write a string as information output. * @@ -332,7 +365,18 @@ public function alert($string) $this->comment('* '.$string.' *'); $this->comment(str_repeat('*', $length)); - $this->output->newLine(); + $this->newLine(); + } + + /** + * Write a blank line. + * + * @param int $count + * @return void + */ + public function newLine($count = 1) + { + $this->output->newLine($count); } /** diff --git a/src/Illuminate/Console/Events/ScheduledTaskFailed.php b/src/Illuminate/Console/Events/ScheduledTaskFailed.php index 992d339f1a92..46857ad849a7 100644 --- a/src/Illuminate/Console/Events/ScheduledTaskFailed.php +++ b/src/Illuminate/Console/Events/ScheduledTaskFailed.php @@ -24,8 +24,9 @@ class ScheduledTaskFailed /** * Create a new event instance. * - * @param \Illuminate\Console\Scheduling\Event $task - * @param \Throwable $exception + * @param \Illuminate\Console\Scheduling\Event $task + * @param \Throwable $exception + * @return void */ public function __construct(Event $task, Throwable $exception) { diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index 534212287a7b..e426c9f169d1 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -25,7 +25,7 @@ abstract class GeneratorCommand extends Command /** * Reserved names that cannot be used for generation. * - * @var array + * @var string[] */ protected $reservedNames = [ '__halt_compiler', diff --git a/src/Illuminate/Console/Scheduling/CallbackEvent.php b/src/Illuminate/Console/Scheduling/CallbackEvent.php index cf5c80f15764..dde5d7dea549 100644 --- a/src/Illuminate/Console/Scheduling/CallbackEvent.php +++ b/src/Illuminate/Console/Scheduling/CallbackEvent.php @@ -3,6 +3,7 @@ namespace Illuminate\Console\Scheduling; use Illuminate\Contracts\Container\Container; +use Illuminate\Support\Reflector; use InvalidArgumentException; use LogicException; use Throwable; @@ -36,7 +37,7 @@ class CallbackEvent extends Event */ public function __construct(EventMutex $mutex, $callback, array $parameters = [], $timezone = null) { - if (! is_string($callback) && ! is_callable($callback)) { + if (! is_string($callback) && ! Reflector::isCallable($callback)) { throw new InvalidArgumentException( 'Invalid scheduled callback event. Must be a string or callable.' ); @@ -170,6 +171,6 @@ public function getSummaryForDisplay() return $this->description; } - return is_string($this->callback) ? $this->callback : 'Closure'; + return is_string($this->callback) ? $this->callback : 'Callback'; } } diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 8e8a3e26923a..0bfaeaf8c429 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -12,6 +12,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Date; +use Illuminate\Support\Reflector; use Illuminate\Support\Stringable; use Illuminate\Support\Traits\Macroable; use Illuminate\Support\Traits\ReflectsClosures; @@ -674,7 +675,7 @@ public function onOneServer() */ public function when($callback) { - $this->filters[] = is_callable($callback) ? $callback : function () use ($callback) { + $this->filters[] = Reflector::isCallable($callback) ? $callback : function () use ($callback) { return $callback; }; @@ -689,7 +690,7 @@ public function when($callback) */ public function skip($callback) { - $this->rejects[] = is_callable($callback) ? $callback : function () use ($callback) { + $this->rejects[] = Reflector::isCallable($callback) ? $callback : function () use ($callback) { return $callback; }; diff --git a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php index 6972642c051b..7e1cdf17c0a8 100644 --- a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php +++ b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php @@ -276,7 +276,7 @@ public function twiceDaily($first = 1, $second = 13) */ public function weekdays() { - return $this->spliceIntoPosition(5, '1-5'); + return $this->days(Schedule::MONDAY.'-'.Schedule::FRIDAY); } /** @@ -286,7 +286,7 @@ public function weekdays() */ public function weekends() { - return $this->spliceIntoPosition(5, '0,6'); + return $this->days(Schedule::SATURDAY.','.Schedule::SUNDAY); } /** @@ -296,7 +296,7 @@ public function weekends() */ public function mondays() { - return $this->days(1); + return $this->days(Schedule::MONDAY); } /** @@ -306,7 +306,7 @@ public function mondays() */ public function tuesdays() { - return $this->days(2); + return $this->days(Schedule::TUESDAY); } /** @@ -316,7 +316,7 @@ public function tuesdays() */ public function wednesdays() { - return $this->days(3); + return $this->days(Schedule::WEDNESDAY); } /** @@ -326,7 +326,7 @@ public function wednesdays() */ public function thursdays() { - return $this->days(4); + return $this->days(Schedule::THURSDAY); } /** @@ -336,7 +336,7 @@ public function thursdays() */ public function fridays() { - return $this->days(5); + return $this->days(Schedule::FRIDAY); } /** @@ -346,7 +346,7 @@ public function fridays() */ public function saturdays() { - return $this->days(6); + return $this->days(Schedule::SATURDAY); } /** @@ -356,7 +356,7 @@ public function saturdays() */ public function sundays() { - return $this->days(0); + return $this->days(Schedule::SUNDAY); } /** @@ -374,15 +374,15 @@ public function weekly() /** * Schedule the event to run weekly on a given day and time. * - * @param int $day + * @param int $dayOfWeek * @param string $time * @return $this */ - public function weeklyOn($day, $time = '0:0') + public function weeklyOn($dayOfWeek, $time = '0:0') { $this->dailyAt($time); - return $this->spliceIntoPosition(5, $day); + return $this->days($dayOfWeek); } /** @@ -400,15 +400,15 @@ public function monthly() /** * Schedule the event to run monthly on a given day and time. * - * @param int $day + * @param int $dayOfMonth * @param string $time * @return $this */ - public function monthlyOn($day = 1, $time = '0:0') + public function monthlyOn($dayOfMonth = 1, $time = '0:0') { $this->dailyAt($time); - return $this->spliceIntoPosition(3, $day); + return $this->spliceIntoPosition(3, $dayOfMonth); } /** @@ -421,13 +421,11 @@ public function monthlyOn($day = 1, $time = '0:0') */ public function twiceMonthly($first = 1, $second = 16, $time = '0:0') { - $days = $first.','.$second; + $daysOfMonth = $first.','.$second; $this->dailyAt($time); - return $this->spliceIntoPosition(1, 0) - ->spliceIntoPosition(2, 0) - ->spliceIntoPosition(3, $days); + return $this->spliceIntoPosition(3, $daysOfMonth); } /** @@ -469,6 +467,22 @@ public function yearly() ->spliceIntoPosition(4, 1); } + /** + * Schedule the event to run yearly on a given month, day, and time. + * + * @param int $month + * @param int|string $dayOfMonth + * @param string $time + * @return $this + */ + public function yearlyOn($month = 1, $dayOfMonth = 1, $time = '0:0') + { + $this->dailyAt($time); + + return $this->spliceIntoPosition(3, $dayOfMonth) + ->spliceIntoPosition(4, $month); + } + /** * Set the days of the week the command should run on. * diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 89ad97209ae3..96bc4dce3ef0 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -19,6 +19,14 @@ class Schedule { use Macroable; + const SUNDAY = 0; + const MONDAY = 1; + const TUESDAY = 2; + const WEDNESDAY = 3; + const THURSDAY = 4; + const FRIDAY = 5; + const SATURDAY = 6; + /** * All of the events on the schedule. * 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/Scheduling/ScheduleWorkCommand.php b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php new file mode 100644 index 000000000000..ec296ebd4972 --- /dev/null +++ b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php @@ -0,0 +1,68 @@ +info('Schedule worker started successfully.'); + + [$lastExecutionStartedAt, $keyOfLastExecutionWithOutput, $executions] = [null, null, []]; + + while (true) { + usleep(100 * 1000); + + if (Carbon::now()->second === 0 && + ! Carbon::now()->startOfMinute()->equalTo($lastExecutionStartedAt)) { + $executions[] = $execution = new Process([PHP_BINARY, 'artisan', 'schedule:run']); + + $execution->start(); + + $lastExecutionStartedAt = Carbon::now()->startOfMinute(); + } + + foreach ($executions as $key => $execution) { + $output = trim($execution->getIncrementalOutput()). + trim($execution->getIncrementalErrorOutput()); + + if (! empty($output)) { + if ($key !== $keyOfLastExecutionWithOutput) { + $this->info(PHP_EOL.'Execution #'.($key + 1).' output:'); + + $keyOfLastExecutionWithOutput = $key; + } + + $this->output->writeln($output); + } + + if (! $execution->isRunning()) { + unset($executions[$key]); + } + } + } + } +} diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index f9fb8b5b4343..46aaada73dd9 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -14,13 +14,13 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "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": { @@ -33,7 +33,7 @@ } }, "suggest": { - "dragonmantank/cron-expression": "Required to use scheduler (^3.0).", + "dragonmantank/cron-expression": "Required to use scheduler (^3.0.2).", "guzzlehttp/guzzle": "Required to use the ping methods on schedules (^6.5.5|^7.0.1).", "illuminate/bus": "Required to use the scheduled job dispatcher (^8.0).", "illuminate/container": "Required to use the scheduler (^8.0).", diff --git a/src/Illuminate/Container/BoundMethod.php b/src/Illuminate/Container/BoundMethod.php index 5da3d7655372..c617bf79795f 100644 --- a/src/Illuminate/Container/BoundMethod.php +++ b/src/Illuminate/Container/BoundMethod.php @@ -33,9 +33,7 @@ public static function call($container, $callback, array $parameters = [], $defa } return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) { - return call_user_func_array( - $callback, static::getMethodDependencies($container, $callback, $parameters) - ); + return $callback(...array_values(static::getMethodDependencies($container, $callback, $parameters))); }); } @@ -126,7 +124,7 @@ protected static function getMethodDependencies($container, $callback, array $pa static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies); } - return array_merge($dependencies, $parameters); + return array_merge($dependencies, array_values($parameters)); } /** diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 98d8f0ae7900..765df0d873d8 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -105,6 +105,13 @@ class Container implements ArrayAccess, ContainerContract */ protected $reboundCallbacks = []; + /** + * All of the global before resolving callbacks. + * + * @var \Closure[] + */ + protected $globalBeforeResolvingCallbacks = []; + /** * All of the global resolving callbacks. * @@ -119,6 +126,13 @@ class Container implements ArrayAccess, ContainerContract */ protected $globalAfterResolvingCallbacks = []; + /** + * All of the before resolving callbacks by class type. + * + * @var array[] + */ + protected $beforeResolvingCallbacks = []; + /** * All of the resolving callbacks by class type. * @@ -612,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 * @@ -626,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 * @@ -656,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 @@ -667,6 +681,13 @@ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { $abstract = $this->getAlias($abstract); + // 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) { + $this->fireBeforeResolvingCallbacks($abstract, $parameters); + } + $concrete = $this->getContextualConcrete($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null($concrete); @@ -724,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) @@ -742,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) @@ -768,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) @@ -1032,6 +1053,26 @@ protected function unresolvablePrimitive(ReflectionParameter $parameter) throw new BindingResolutionException($message); } + /** + * Register a new before resolving callback for all types. + * + * @param \Closure|string $abstract + * @param \Closure|null $callback + * @return void + */ + public function beforeResolving($abstract, Closure $callback = null) + { + if (is_string($abstract)) { + $abstract = $this->getAlias($abstract); + } + + if ($abstract instanceof Closure && is_null($callback)) { + $this->globalBeforeResolvingCallbacks[] = $abstract; + } else { + $this->beforeResolvingCallbacks[$abstract][] = $callback; + } + } + /** * Register a new resolving callback. * @@ -1072,6 +1113,39 @@ public function afterResolving($abstract, Closure $callback = null) } } + /** + * Fire all of the before resolving callbacks. + * + * @param string $abstract + * @param array $parameters + * @return void + */ + protected function fireBeforeResolvingCallbacks($abstract, $parameters = []) + { + $this->fireBeforeCallbackArray($abstract, $parameters, $this->globalBeforeResolvingCallbacks); + + foreach ($this->beforeResolvingCallbacks as $type => $callbacks) { + if ($type === $abstract || is_subclass_of($abstract, $type)) { + $this->fireBeforeCallbackArray($abstract, $parameters, $callbacks); + } + } + } + + /** + * Fire an array of callbacks with an object. + * + * @param string $abstract + * @param array $parameters + * @param array $callbacks + * @return void + */ + protected function fireBeforeCallbackArray($abstract, $parameters, array $callbacks) + { + foreach ($callbacks as $callback) { + $callback($abstract, $parameters, $this); + } + } + /** * Fire all of the resolving callbacks. * diff --git a/src/Illuminate/Container/composer.json b/src/Illuminate/Container/composer.json index ce6d44868e16..cf93160996bf 100755 --- a/src/Illuminate/Container/composer.json +++ b/src/Illuminate/Container/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/contracts": "^8.0", "psr/container": "^1.0" }, diff --git a/src/Illuminate/Contracts/Auth/StatefulGuard.php b/src/Illuminate/Contracts/Auth/StatefulGuard.php index eb6f8ddb3cb0..faf1497d5f84 100644 --- a/src/Illuminate/Contracts/Auth/StatefulGuard.php +++ b/src/Illuminate/Contracts/Auth/StatefulGuard.php @@ -35,7 +35,7 @@ public function login(Authenticatable $user, $remember = false); * * @param mixed $id * @param bool $remember - * @return \Illuminate\Contracts\Auth\Authenticatable + * @return \Illuminate\Contracts\Auth\Authenticatable|bool */ public function loginUsingId($id, $remember = false); diff --git a/src/Illuminate/Contracts/Cache/Lock.php b/src/Illuminate/Contracts/Cache/Lock.php index 4f98d68d9301..7f01b1be3f33 100644 --- a/src/Illuminate/Contracts/Cache/Lock.php +++ b/src/Illuminate/Contracts/Cache/Lock.php @@ -24,7 +24,7 @@ public function block($seconds, $callback = null); /** * Release the lock. * - * @return void + * @return bool */ public function release(); diff --git a/src/Illuminate/Contracts/Database/Eloquent/DeviatesCastableAttributes.php b/src/Illuminate/Contracts/Database/Eloquent/DeviatesCastableAttributes.php new file mode 100644 index 000000000000..48ba73abc369 --- /dev/null +++ b/src/Illuminate/Contracts/Database/Eloquent/DeviatesCastableAttributes.php @@ -0,0 +1,28 @@ +make(...$parameters); + $cookie = $this->make(...array_values($parameters)); } if (! isset($this->queued[$cookie->getName()])) { diff --git a/src/Illuminate/Cookie/Middleware/EncryptCookies.php b/src/Illuminate/Cookie/Middleware/EncryptCookies.php index c286588479aa..4a116cfb3301 100644 --- a/src/Illuminate/Cookie/Middleware/EncryptCookies.php +++ b/src/Illuminate/Cookie/Middleware/EncryptCookies.php @@ -76,7 +76,7 @@ public function handle($request, Closure $next) protected function decrypt(Request $request) { foreach ($request->cookies as $key => $cookie) { - if ($this->isDisabled($key)) { + if ($this->isDisabled($key) || is_array($cookie)) { continue; } diff --git a/src/Illuminate/Cookie/composer.json b/src/Illuminate/Cookie/composer.json index 3c524f05af0e..d90265905a02 100755 --- a/src/Illuminate/Cookie/composer.json +++ b/src/Illuminate/Cookie/composer.json @@ -14,13 +14,13 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0", - "symfony/http-foundation": "^5.1", - "symfony/http-kernel": "^5.1" + "symfony/http-foundation": "^5.1.4", + "symfony/http-kernel": "^5.1.4" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Database/Concerns/ExplainsQueries.php b/src/Illuminate/Database/Concerns/ExplainsQueries.php new file mode 100644 index 000000000000..7168de1e55cf --- /dev/null +++ b/src/Illuminate/Database/Concerns/ExplainsQueries.php @@ -0,0 +1,24 @@ +toSql(); + + $bindings = $this->getBindings(); + + $explanation = $this->getConnection()->select('EXPLAIN '.$sql, $bindings); + + return new Collection($explanation); + } +} 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 6c7569225eac..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. * @@ -891,7 +898,13 @@ public function getDoctrineColumn($table, $column) */ public function getDoctrineSchemaManager() { - return $this->getDoctrineDriver()->getSchemaManager($this->getDoctrineConnection()); + $connection = $this->getDoctrineConnection(); + + // Doctrine v2 expects one parameter while v3 expects two. 2nd will be ignored on v2... + return $this->getDoctrineDriver()->getSchemaManager( + $connection, + $connection->getDatabasePlatform() + ); } /** @@ -907,7 +920,7 @@ public function getDoctrineConnection() $this->doctrineConnection = new DoctrineConnection(array_filter([ 'pdo' => $this->getPdo(), 'dbname' => $this->getDatabaseName(), - 'driver' => $driver->getName(), + 'driver' => method_exists($driver, 'getName') ? $driver->getName() : null, 'serverVersion' => $this->getConfig('server_version'), ]), $driver); } @@ -1145,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 new file mode 100644 index 000000000000..9152d1dc844c --- /dev/null +++ b/src/Illuminate/Database/Console/DbCommand.php @@ -0,0 +1,196 @@ +getConnection(); + + (new Process( + array_merge([$this->getCommand($connection)], $this->commandArguments($connection)), + null, + $this->commandEnvironment($connection) + ))->setTimeout(null)->setTty(true)->mustRun(function ($type, $buffer) { + $this->output->write($buffer); + }); + + return 0; + } + + /** + * Get the database connection configuration. + * + * @return array + */ + public function getConnection() + { + $connection = $this->laravel['config']['database.connections.'. + (($db = $this->argument('connection')) ?? $this->laravel['config']['database.default']) + ]; + + if (empty($connection)) { + throw new UnexpectedValueException("Invalid database connection [{$db}]."); + } + + return $connection; + } + + /** + * Get the arguments for the database client command. + * + * @param array $connection + * @return array + */ + public function commandArguments(array $connection) + { + $driver = ucfirst($connection['driver']); + + return $this->{"get{$driver}Arguments"}($connection); + } + + /** + * Get the environment variables for the database client command. + * + * @param array $connection + * @return array|null + */ + public function commandEnvironment(array $connection) + { + $driver = ucfirst($connection['driver']); + + if (method_exists($this, "get{$driver}Environment")) { + return $this->{"get{$driver}Environment"}($connection); + } + + return null; + } + + /** + * Get the database client command to run. + * + * @param array $connection + * @return string + */ + public function getCommand(array $connection) + { + return [ + 'mysql' => 'mysql', + 'pgsql' => 'psql', + 'sqlite' => 'sqlite3', + 'sqlsrv' => 'sqlcmd', + ][$connection['driver']]; + } + + /** + * Get the arguments for the MySQL CLI. + * + * @param array $connection + * @return array + */ + protected function getMysqlArguments(array $connection) + { + return array_merge([ + '--host='.$connection['host'], + '--port='.$connection['port'], + '--user='.$connection['username'], + ], $this->getOptionalArguments([ + 'password' => '--password='.$connection['password'], + 'unix_socket' => '--socket='.$connection['unix_socket'], + 'charset' => '--default-character-set='.$connection['charset'], + ], $connection), [$connection['database']]); + } + + /** + * Get the arguments for the Postgres CLI. + * + * @param array $connection + * @return array + */ + protected function getPgsqlArguments(array $connection) + { + return [$connection['database']]; + } + + /** + * Get the arguments for the SQLite CLI. + * + * @param array $connection + * @return array + */ + protected function getSqliteArguments(array $connection) + { + return [$connection['database']]; + } + + /** + * Get the arguments for the SQL Server CLI. + * + * @param array $connection + * @return array + */ + protected function getSqlsrvArguments(array $connection) + { + return array_merge(...$this->getOptionalArguments([ + 'database' => ['-d', $connection['database']], + 'username' => ['-U', $connection['username']], + 'password' => ['-P', $connection['password']], + 'host' => ['-S', 'tcp:'.$connection['host'] + .($connection['port'] ? ','.$connection['port'] : ''), ], + ], $connection)); + } + + /** + * Get the environment variables for the Postgres CLI. + * + * @param array $connection + * @return array|null + */ + protected function getPgsqlEnvironment(array $connection) + { + return array_merge(...$this->getOptionalArguments([ + 'username' => ['PGUSER' => $connection['username']], + 'host' => ['PGHOST' => $connection['host']], + 'port' => ['PGPORT' => $connection['port']], + 'password' => ['PGPASSWORD' => $connection['password']], + ], $connection)); + } + + /** + * Get the optional arguments based on the connection configuration. + * + * @param array $args + * @param array $connection + * @return array + */ + protected function getOptionalArguments(array $args, array $connection) + { + return array_values(array_filter($args, function ($key) use ($connection) { + return ! empty($connection[$key]); + }, ARRAY_FILTER_USE_KEY)); + } +} diff --git a/src/Illuminate/Database/Console/DumpCommand.php b/src/Illuminate/Database/Console/DumpCommand.php index 7662823443e6..fe73fb2af033 100644 --- a/src/Illuminate/Database/Console/DumpCommand.php +++ b/src/Illuminate/Database/Console/DumpCommand.php @@ -8,6 +8,7 @@ use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Events\SchemaDumped; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\Config; class DumpCommand extends Command { @@ -31,13 +32,17 @@ 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) { - $this->schemaState( - $connection = $connections->connection($database = $this->input->getOption('database')) - )->dump($path = $this->path($connection)); + $connection = $connections->connection($database = $this->input->getOption('database')); + + $this->schemaState($connection)->dump( + $connection, $path = $this->path($connection) + ); $dispatcher->dispatch(new SchemaDumped($connection, $path)); @@ -61,6 +66,7 @@ public function handle(ConnectionResolverInterface $connections, Dispatcher $dis protected function schemaState(Connection $connection) { return $connection->getSchemaState() + ->withMigrationTable($connection->getTablePrefix().Config::get('database.migrations', 'migrations')) ->handleOutputUsing(function ($type, $buffer) { $this->output->write($buffer); }); @@ -73,7 +79,7 @@ protected function schemaState(Connection $connection) */ protected function path(Connection $connection) { - return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.sql'), function ($path) { + return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.dump'), function ($path) { (new Filesystem)->ensureDirectoryExists(dirname($path)); }); } diff --git a/src/Illuminate/Database/Console/Factories/FactoryMakeCommand.php b/src/Illuminate/Database/Console/Factories/FactoryMakeCommand.php index 88b9b4a0bba4..6233fe29f07d 100644 --- a/src/Illuminate/Database/Console/Factories/FactoryMakeCommand.php +++ b/src/Illuminate/Database/Console/Factories/FactoryMakeCommand.php @@ -60,14 +60,16 @@ protected function resolveStubPath($stub) */ protected function buildClass($name) { + $factory = class_basename(Str::ucfirst(str_replace('Factory', '', $name))); + $namespaceModel = $this->option('model') ? $this->qualifyModel($this->option('model')) : $this->qualifyModel($this->guessModelName($name)); $model = class_basename($namespaceModel); - if (Str::startsWith($namespaceModel, 'App\\Models')) { - $namespace = Str::beforeLast('Database\\Factories\\'.Str::after($namespaceModel, 'App\\Models\\'), '\\'); + if (Str::startsWith($namespaceModel, $this->rootNamespace().'Models')) { + $namespace = Str::beforeLast('Database\\Factories\\'.Str::after($namespaceModel, $this->rootNamespace().'Models\\'), '\\'); } else { $namespace = 'Database\\Factories'; } @@ -80,6 +82,8 @@ protected function buildClass($name) 'DummyModel' => $model, '{{ model }}' => $model, '{{model}}' => $model, + '{{ factory }}' => $factory, + '{{factory}}' => $factory, ]; return str_replace( @@ -95,9 +99,7 @@ protected function buildClass($name) */ protected function getPath($name) { - $name = Str::replaceFirst('App\\', '', $name); - - $name = Str::finish($this->argument('name'), 'Factory'); + $name = (string) Str::of($name)->replaceFirst($this->rootNamespace(), '')->finish('Factory'); return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php'; } @@ -114,17 +116,17 @@ protected function guessModelName($name) $name = substr($name, 0, -7); } - $modelName = $this->qualifyModel(class_basename($name)); + $modelName = $this->qualifyModel(Str::after($name, $this->rootNamespace())); if (class_exists($modelName)) { return $modelName; } if (is_dir(app_path('Models/'))) { - return 'App\Models\Model'; + return $this->rootNamespace().'Models\Model'; } - return 'App\Model'; + return $this->rootNamespace().'Model'; } /** diff --git a/src/Illuminate/Database/Console/Factories/stubs/factory.stub b/src/Illuminate/Database/Console/Factories/stubs/factory.stub index b85cdf4b45fd..f7a898c9f1fe 100644 --- a/src/Illuminate/Database/Console/Factories/stubs/factory.stub +++ b/src/Illuminate/Database/Console/Factories/stubs/factory.stub @@ -5,7 +5,7 @@ namespace {{ factoryNamespace }}; use Illuminate\Database\Eloquent\Factories\Factory; use {{ namespacedModel }}; -class {{ model }}Factory extends Factory +class {{ factory }}Factory extends Factory { /** * The name of the factory's corresponding model. diff --git a/src/Illuminate/Database/Console/Migrations/FreshCommand.php b/src/Illuminate/Database/Console/Migrations/FreshCommand.php index 58c28614bcc1..7bfba0d78821 100644 --- a/src/Illuminate/Database/Console/Migrations/FreshCommand.php +++ b/src/Illuminate/Database/Console/Migrations/FreshCommand.php @@ -4,6 +4,8 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Events\DatabaseRefreshed; use Symfony\Component\Console\Input\InputOption; class FreshCommand extends Command @@ -53,6 +55,12 @@ public function handle() '--step' => $this->option('step'), ])); + if ($this->laravel->bound(Dispatcher::class)) { + $this->laravel[Dispatcher::class]->dispatch( + new DatabaseRefreshed + ); + } + if ($this->needsSeeding()) { $this->runSeeder($database); } diff --git a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php index e18835301687..ec35f8fed162 100755 --- a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php @@ -164,6 +164,14 @@ protected function loadSchemaState() */ protected function schemaPath($connection) { - return $this->option('schema-path') ?: database_path('schema/'.$connection->getName().'-schema.sql'); + if ($this->option('schema-path')) { + return $this->option('schema-path'); + } + + if (file_exists($path = database_path('schema/'.$connection->getName().'-schema.dump'))) { + return $path; + } + + return database_path('schema/'.$connection->getName().'-schema.sql'); } } diff --git a/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php index 2c2a71155ff7..95c3a206e54a 100644 --- a/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php @@ -132,14 +132,4 @@ protected function getMigrationPath() return parent::getMigrationPath(); } - - /** - * Determine if the given path(s) are pre-resolved "real" paths. - * - * @return bool - */ - protected function usingRealPath() - { - return $this->input->hasOption('realpath') && $this->option('realpath'); - } } diff --git a/src/Illuminate/Database/Console/Migrations/RefreshCommand.php b/src/Illuminate/Database/Console/Migrations/RefreshCommand.php index 1c210eb8a584..2073cd9977e6 100755 --- a/src/Illuminate/Database/Console/Migrations/RefreshCommand.php +++ b/src/Illuminate/Database/Console/Migrations/RefreshCommand.php @@ -4,6 +4,8 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Events\DatabaseRefreshed; use Symfony\Component\Console\Input\InputOption; class RefreshCommand extends Command @@ -63,6 +65,12 @@ public function handle() '--force' => true, ])); + if ($this->laravel->bound(Dispatcher::class)) { + $this->laravel[Dispatcher::class]->dispatch( + new DatabaseRefreshed + ); + } + if ($this->needsSeeding()) { $this->runSeeder($database); } diff --git a/src/Illuminate/Database/Console/Migrations/ResetCommand.php b/src/Illuminate/Database/Console/Migrations/ResetCommand.php index 21b532979149..1f2babbc8d08 100755 --- a/src/Illuminate/Database/Console/Migrations/ResetCommand.php +++ b/src/Illuminate/Database/Console/Migrations/ResetCommand.php @@ -67,8 +67,6 @@ public function handle() $this->getMigrationPaths(), $this->option('pretend') ); }); - - return 0; } /** 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 72132c164df7..07630c590d5c 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -40,11 +40,15 @@ protected function causedByLostConnection(Throwable $e) 'Communication link failure', 'connection is no longer usable', 'Login timeout expired', - 'Connection refused', + 'SQLSTATE[HY000] [2002] Connection refused', '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 77b41e0fa1fa..d3f1f96a8c13 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -7,6 +7,8 @@ use Exception; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Concerns\BuildsQueries; +use Illuminate\Database\Concerns\ExplainsQueries; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Pagination\Paginator; @@ -23,7 +25,7 @@ */ class Builder { - use BuildsQueries, Concerns\QueriesRelationships, ForwardsCalls; + use BuildsQueries, Concerns\QueriesRelationships, ExplainsQueries, ForwardsCalls; /** * The base query builder instance. @@ -70,7 +72,7 @@ class Builder /** * The methods that should be returned from query builder. * - * @var array + * @var string[] */ protected $passthru = [ 'insert', 'insertOrIgnore', 'insertGetId', 'insertUsing', 'getBindings', 'toSql', 'dump', 'dd', @@ -224,7 +226,7 @@ public function whereKeyNot($id) /** * Add a basic where clause to the query. * - * @param \Closure|string|array $column + * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @param string $boolean @@ -246,7 +248,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' /** * Add a basic where clause to the query, and return the first result. * - * @param \Closure|string|array $column + * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @param string $boolean @@ -260,7 +262,7 @@ public function firstWhere($column, $operator = null, $value = null, $boolean = /** * Add an "or where" clause to the query. * - * @param \Closure|array|string $column + * @param \Closure|array|string|\Illuminate\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @return $this @@ -277,7 +279,7 @@ public function orWhere($column, $operator = null, $value = null) /** * Add an "order by" clause for a timestamp to the query. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @return $this */ public function latest($column = null) @@ -294,7 +296,7 @@ public function latest($column = null) /** * Add an "order by" clause for a timestamp to the query. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @return $this */ public function oldest($column = null) @@ -505,13 +507,13 @@ public function firstOr($columns = ['*'], Closure $callback = null) /** * Get a single column's value from the first result of a query. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @return mixed */ public function value($column) { if ($result = $this->first([$column])) { - return $result->{$column}; + return $result->{Str::afterLast($column, '.')}; } } @@ -688,7 +690,7 @@ protected function enforceOrderBy() /** * Get an array with the values of a given column. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @param string|null $key * @return \Illuminate\Support\Collection */ @@ -800,10 +802,39 @@ public function update(array $values) return $this->toBase()->update($this->addUpdatedAtColumn($values)); } + /** + * Insert new records or update the existing ones. + * + * @param array $values + * @param array|string $uniqueBy + * @param array|null $update + * @return int + */ + public function upsert(array $values, $uniqueBy, $update = null) + { + if (empty($values)) { + return 0; + } + + if (! is_array(reset($values))) { + $values = [$values]; + } + + if (is_null($update)) { + $update = array_keys(reset($values)); + } + + return $this->toBase()->upsert( + $this->addTimestampsToUpsertValues($values), + $uniqueBy, + $this->addUpdatedAtToUpsertColumns($update) + ); + } + /** * Increment a column's value by a given amount. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @param float|int $amount * @param array $extra * @return int @@ -818,7 +849,7 @@ public function increment($column, $amount = 1, array $extra = []) /** * Decrement a column's value by a given amount. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @param float|int $amount * @param array $extra * @return int @@ -861,6 +892,57 @@ protected function addUpdatedAtColumn(array $values) return $values; } + /** + * Add timestamps to the inserted values. + * + * @param array $values + * @return array + */ + protected function addTimestampsToUpsertValues(array $values) + { + if (! $this->model->usesTimestamps()) { + return $values; + } + + $timestamp = $this->model->freshTimestampString(); + + $columns = array_filter([ + $this->model->getCreatedAtColumn(), + $this->model->getUpdatedAtColumn(), + ]); + + foreach ($columns as $column) { + foreach ($values as &$row) { + $row = array_merge([$column => $timestamp], $row); + } + } + + return $values; + } + + /** + * Add the "updated at" column to the updated columns. + * + * @param array $update + * @return array + */ + protected function addUpdatedAtToUpsertColumns(array $update) + { + if (! $this->model->usesTimestamps()) { + return $update; + } + + $column = $this->model->getUpdatedAtColumn(); + + if (! is_null($column) && + ! array_key_exists($column, $update) && + ! in_array($column, $update)) { + $update[] = $column; + } + + return $update; + } + /** * Delete records from the database. * @@ -1172,7 +1254,15 @@ protected function parseWithRelations(array $relations) protected function createSelectWithConstraint($name) { return [explode(':', $name)[0], static function ($query) use ($name) { - $query->select(explode(',', explode(':', $name)[1])); + $query->select(array_map(static function ($column) use ($query) { + if (Str::contains($column, '.')) { + return $column; + } + + return $query instanceof BelongsToMany + ? $query->getRelated()->getTable().'.'.$column + : $column; + }, explode(',', explode(':', $name)[1]))); }]; } @@ -1310,7 +1400,7 @@ public function setModel(Model $model) /** * Qualify the given column name by the model's table. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @return string */ public function qualifyColumn($column) @@ -1401,11 +1491,13 @@ public function __call($method, $parameters) } if (static::hasGlobalMacro($method)) { - if (static::$macros[$method] instanceof Closure) { - return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters); + $callable = static::$macros[$method]; + + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this, static::class); } - return call_user_func_array(static::$macros[$method], $parameters); + return $callable(...$parameters); } if ($this->hasNamedScope($method)) { @@ -1446,11 +1538,13 @@ public static function __callStatic($method, $parameters) static::throwBadMethodCallException($method); } - if (static::$macros[$method] instanceof Closure) { - return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters); + $callable = static::$macros[$method]; + + if ($callable instanceof Closure) { + $callable = $callable->bindTo(null, static::class); } - return call_user_func_array(static::$macros[$method], $parameters); + return $callable(...$parameters); } /** diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 70c90e97ce92..05c785eff15b 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -64,12 +64,14 @@ public function load($relations) } /** - * Load a set of relationship counts onto the collection. + * Load a set of aggregations over relationship's column onto the collection. * * @param array|string $relations + * @param string $column + * @param string $function * @return $this */ - public function loadCount($relations) + public function loadAggregate($relations, $column, $function = null) { if ($this->isEmpty()) { return $this; @@ -78,7 +80,7 @@ public function loadCount($relations) $models = $this->first()->newModelQuery() ->whereKey($this->modelKeys()) ->select($this->first()->getKeyName()) - ->withCount(...func_get_args()) + ->withAggregate($relations, $column, $function) ->get() ->keyBy($this->first()->getKeyName()); @@ -96,6 +98,65 @@ public function loadCount($relations) return $this; } + /** + * Load a set of relationship counts onto the collection. + * + * @param array|string $relations + * @return $this + */ + public function loadCount($relations) + { + return $this->loadAggregate($relations, '*', 'count'); + } + + /** + * Load a set of relationship's max column values onto the collection. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadMax($relations, $column) + { + return $this->loadAggregate($relations, $column, 'max'); + } + + /** + * Load a set of relationship's min column values onto the collection. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadMin($relations, $column) + { + return $this->loadAggregate($relations, $column, 'min'); + } + + /** + * Load a set of relationship's column summations onto the collection. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadSum($relations, $column) + { + return $this->loadAggregate($relations, $column, 'sum'); + } + + /** + * Load a set of relationship's average column values onto the collection. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadAvg($relations, $column) + { + return $this->loadAggregate($relations, $column, 'avg'); + } + /** * Load a set of relationships onto the collection if they are not already eager loaded. * @@ -280,6 +341,23 @@ public function map(callable $callback) }) ? $result->toBase() : $result; } + /** + * Run an associative map over each of the items. + * + * The callback should return an associative array with a single key / value pair. + * + * @param callable $callback + * @return \Illuminate\Support\Collection|static + */ + public function mapWithKeys(callable $callback) + { + $result = parent::mapWithKeys($callback); + + return $result->contains(function ($item) { + return ! $item instanceof Model; + }) ? $result->toBase() : $result; + } + /** * Reload a fresh model instance from the database for all the entities. * @@ -300,9 +378,11 @@ public function fresh($with = []) ->get() ->getDictionary(); - return $this->map(function ($model) use ($freshModels) { - return $model->exists && isset($freshModels[$model->getKey()]) - ? $freshModels[$model->getKey()] : null; + return $this->filter(function ($model) use ($freshModels) { + return $model->exists && isset($freshModels[$model->getKey()]); + }) + ->map(function ($model) use ($freshModels) { + return $freshModels[$model->getKey()]; }); } @@ -484,7 +564,7 @@ public function keys() */ public function zip($items) { - return call_user_func_array([$this->toBase(), 'zip'], func_get_args()); + return $this->toBase()->zip(...func_get_args()); } /** diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index 77f71d1df836..9cbee56764ea 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -9,14 +9,14 @@ trait GuardsAttributes /** * The attributes that are mass assignable. * - * @var array + * @var string[] */ protected $fillable = []; /** * The attributes that aren't mass assignable. * - * @var array|bool + * @var string[]|bool */ protected $guarded = ['*']; diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 0fe957aebe19..0bde3e119227 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -13,8 +13,10 @@ use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Collection as BaseCollection; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; +use InvalidArgumentException; use LogicException; trait HasAttributes @@ -57,7 +59,7 @@ trait HasAttributes /** * The built-in, primitive cast types supported by Eloquent. * - * @var array + * @var string[] */ protected static $primitiveCastTypes = [ 'array', @@ -69,6 +71,11 @@ trait HasAttributes 'datetime', 'decimal', 'double', + 'encrypted', + 'encrypted:array', + 'encrypted:collection', + 'encrypted:json', + 'encrypted:object', 'float', 'int', 'integer', @@ -116,6 +123,13 @@ trait HasAttributes */ protected static $mutatorCache = []; + /** + * The encrypter instance that is used to encrypt attributes. + * + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + public static $encrypter; + /** * Convert the model's attributes to an array. * @@ -239,6 +253,10 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt $attributes[$key] = $this->serializeDate($attributes[$key]); } + if ($attributes[$key] && $this->isClassSerializable($key)) { + $attributes[$key] = $this->serializeClassCastableAttribute($key, $attributes[$key]); + } + if ($attributes[$key] instanceof Arrayable) { $attributes[$key] = $attributes[$key]->toArray(); } @@ -496,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; } /** @@ -518,6 +538,15 @@ protected function castAttribute($key, $value) return $value; } + // If the key is one of the encrypted castable types, we'll first decrypt + // the value and update the cast type so we may leverage the following + // logic for casting this value to any additionally specified types. + if ($this->isEncryptedCastable($key)) { + $value = $this->fromEncryptedString($value); + + $castType = Str::after($castType, 'encrypted:'); + } + switch ($castType) { case 'int': case 'integer': @@ -603,6 +632,35 @@ protected function getCastType($key) return trim(strtolower($this->getCasts()[$key])); } + /** + * Increment or decrement the given attribute using the custom cast class. + * + * @param string $method + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function deviateClassCastableAttribute($method, $key, $value) + { + return $this->resolveCasterClass($key)->{$method}( + $this, $key, $value, $this->attributes + ); + } + + /** + * Serialize the given attribute using the custom cast class. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function serializeClassCastableAttribute($key, $value) + { + return $this->resolveCasterClass($key)->serialize( + $this, $key, $value, $this->attributes + ); + } + /** * Determine if the cast type is a custom date time cast. * @@ -655,7 +713,7 @@ public function setAttribute($key, $value) return $this; } - if ($this->isJsonCastable($key) && ! is_null($value)) { + if (! is_null($value) && $this->isJsonCastable($key)) { $value = $this->castAttributeAsJson($key, $value); } @@ -666,6 +724,10 @@ public function setAttribute($key, $value) return $this->fillJsonAttribute($key, $value); } + if (! is_null($value) && $this->isEncryptedCastable($key)) { + $value = $this->castAttributeAsEncryptedString($key, $value); + } + $this->attributes[$key] = $value; return $this; @@ -829,6 +891,40 @@ public function fromJson($value, $asObject = false) return json_decode($value, ! $asObject); } + /** + * Decrypt the given encrypted string. + * + * @param string $value + * @return mixed + */ + public function fromEncryptedString($value) + { + return (static::$encrypter ?? Crypt::getFacadeRoot())->decrypt($value, false); + } + + /** + * Cast the given attribute to an encrypted string. + * + * @param string $key + * @param mixed $value + * @return string + */ + protected function castAttributeAsEncryptedString($key, $value) + { + return (static::$encrypter ?? Crypt::getFacadeRoot())->encrypt($value, false); + } + + /** + * Set the encrypter instance that will be used to encrypt attributes. + * + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @return void + */ + public static function encryptUsing($encrypter) + { + static::$encrypter = $encrypter; + } + /** * Decode the given float. * @@ -915,11 +1011,13 @@ protected function asDateTime($value) // Finally, we will just assume this date is in the format used by default on // the database connection and use that format to create the Carbon object // that is returned back out to the developers after we convert it here. - if (Date::hasFormat($value, $format)) { - return Date::createFromFormat($format, $value); + try { + $date = Date::createFromFormat($format, $value); + } catch (InvalidArgumentException $e) { + $date = false; } - return Date::parse($value); + return $date ?: Date::parse($value); } /** @@ -1059,7 +1157,18 @@ protected function isDateCastable($key) */ protected function isJsonCastable($key) { - return $this->hasCast($key, ['array', 'json', 'object', 'collection']); + return $this->hasCast($key, ['array', 'json', 'object', 'collection', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']); + } + + /** + * Determine whether a value is an encrypted castable for inbound manipulation. + * + * @param string $key + * @return bool + */ + protected function isEncryptedCastable($key) + { + return $this->hasCast($key, ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']); } /** @@ -1087,6 +1196,35 @@ protected function isClassCastable($key) throw new InvalidCastException($this->getModel(), $key, $castType); } + /** + * Determine if the key is deviable using a custom class. + * + * @param string $key + * @return bool + * + * @throws \Illuminate\Database\Eloquent\InvalidCastException + */ + protected function isClassDeviable($key) + { + return $this->isClassCastable($key) && + method_exists($castType = $this->parseCasterClass($this->getCasts()[$key]), 'increment') && + method_exists($castType, 'decrement'); + } + + /** + * Determine if the key is serializable using a custom class. + * + * @param string $key + * @return bool + * + * @throws \Illuminate\Database\Eloquent\InvalidCastException + */ + protected function isClassSerializable($key) + { + return $this->isClassCastable($key) && + method_exists($this->parseCasterClass($this->getCasts()[$key]), 'serialize'); + } + /** * Resolve the custom caster class for a given key. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 0973c2097887..5262d4305273 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -39,7 +39,7 @@ trait HasRelationships /** * The many to many relationship methods. * - * @var array + * @var string[] */ public static $manyMethods = [ 'belongsToMany', 'morphToMany', 'morphedByMany', @@ -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); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php index f820bf7aa9eb..13ebd31744cd 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php @@ -110,7 +110,7 @@ public function usesTimestamps() /** * Get the name of the "created at" column. * - * @return string + * @return string|null */ public function getCreatedAtColumn() { @@ -120,7 +120,7 @@ public function getCreatedAtColumn() /** * Get the name of the "updated at" column. * - * @return string + * @return string|null */ public function getUpdatedAtColumn() { @@ -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 c97c4033082e..4fbc2f90ebd1 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -9,7 +9,6 @@ use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Expression; use Illuminate\Support\Str; -use RuntimeException; trait QueriesRelationships { @@ -36,7 +35,7 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', C } if ($relation instanceof MorphTo) { - throw new RuntimeException('Please use whereHasMorph() for MorphTo relationships.'); + return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback); } // If we only need to check for the existence of the relation, then we can optimize @@ -189,7 +188,7 @@ public function orWhereDoesntHave($relation, Closure $callback = null) /** * Add a polymorphic relationship count / exists condition to the query. * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param string $operator * @param int $count @@ -199,7 +198,9 @@ public function orWhereDoesntHave($relation, Closure $callback = null) */ public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null) { - $relation = $this->getRelationWithoutConstraints($relation); + if (is_string($relation)) { + $relation = $this->getRelationWithoutConstraints($relation); + } $types = (array) $types; @@ -254,7 +255,7 @@ protected function getBelongsToRelation(MorphTo $relation, $type) /** * Add a polymorphic relationship count / exists condition to the query with an "or". * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param string $operator * @param int $count @@ -268,7 +269,7 @@ public function orHasMorph($relation, $types, $operator = '>=', $count = 1) /** * Add a polymorphic relationship count / exists condition to the query. * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param string $boolean * @param \Closure|null $callback @@ -282,7 +283,7 @@ public function doesntHaveMorph($relation, $types, $boolean = 'and', Closure $ca /** * Add a polymorphic relationship count / exists condition to the query with an "or". * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @return \Illuminate\Database\Eloquent\Builder|static */ @@ -294,7 +295,7 @@ public function orDoesntHaveMorph($relation, $types) /** * Add a polymorphic relationship count / exists condition to the query with where clauses. * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @param string $operator @@ -309,7 +310,7 @@ public function whereHasMorph($relation, $types, Closure $callback = null, $oper /** * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @param string $operator @@ -324,7 +325,7 @@ public function orWhereHasMorph($relation, $types, Closure $callback = null, $op /** * Add a polymorphic relationship count / exists condition to the query with where clauses. * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static @@ -337,7 +338,7 @@ public function whereDoesntHaveMorph($relation, $types, Closure $callback = null /** * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". * - * @param string $relation + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static @@ -348,12 +349,14 @@ public function orWhereDoesntHaveMorph($relation, $types, Closure $callback = nu } /** - * Add subselect queries to count the relations. + * Add subselect queries to include an aggregate value for a relationship. * * @param mixed $relations + * @param string $column + * @param string $function * @return $this */ - public function withCount($relations) + public function withAggregate($relations, $column, $function = null) { if (empty($relations)) { return $this; @@ -363,12 +366,12 @@ public function withCount($relations) $this->query->select([$this->query->from.'.*']); } - $relations = is_array($relations) ? $relations : func_get_args(); + $relations = is_array($relations) ? $relations : [$relations]; foreach ($this->parseWithRelations($relations) as $name => $constraints) { // First we will determine if the name has been aliased using an "as" clause on the name // and if it has we will extract the actual relationship name and the desired name of - // the resulting column. This allows multiple counts on the same relationship name. + // the resulting column. This allows multiple aggregates on the same relationships. $segments = explode(' ', $name); unset($alias); @@ -379,38 +382,115 @@ public function withCount($relations) $relation = $this->getRelationWithoutConstraints($name); - // Here we will get the relationship count query and prepare to add it to the main query + 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($hashedColumn) + )); + } else { + $expression = $column; + } + + // Here, we will grab the relationship sub-query and prepare to add it to the main query // as a sub-select. First, we'll get the "has" query and use that to get the relation - // count query. We will normalize the relation name then append _count as the name. - $query = $relation->getRelationExistenceCountQuery( - $relation->getRelated()->newQuery(), $this - ); + // sub-query. We'll format this relationship name and append this column if needed. + $query = $relation->getRelationExistenceQuery( + $relation->getRelated()->newQuery(), $this, new Expression($expression) + )->setBindings([], 'select'); $query->callScope($constraints); $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase(); + // If the query contains certain elements like orderings / more than one column selected + // then we will remove those elements from the query so that it will execute properly + // when given to the database. Otherwise, we may receive SQL errors or poor syntax. $query->orders = null; - $query->setBindings([], 'order'); if (count($query->columns) > 1) { $query->columns = [$query->columns[0]]; - $query->bindings['select'] = []; } - // Finally we will add the proper result column alias to the query and run the subselect - // statement against the query builder. Then we will return the builder instance back - // to the developer for further constraint chaining that needs to take place on it. - $column = $alias ?? Str::snake($name.'_count'); + // Finally, we will make the proper column alias to the query and run this sub-select on + // the query builder. Then, we will return the builder instance back to the developer + // for further constraint chaining that needs to take place on the query as needed. + $alias = $alias ?? Str::snake( + preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column") + ); - $this->selectSub($query, $column); + $this->selectSub( + $function ? $query : $query->limit(1), + $alias + ); } return $this; } + /** + * Add subselect queries to count the relations. + * + * @param mixed $relations + * @return $this + */ + public function withCount($relations) + { + return $this->withAggregate(is_array($relations) ? $relations : func_get_args(), '*', 'count'); + } + + /** + * Add subselect queries to include the max of the relation's column. + * + * @param string|array $relation + * @param string $column + * @return $this + */ + public function withMax($relation, $column) + { + return $this->withAggregate($relation, $column, 'max'); + } + + /** + * Add subselect queries to include the min of the relation's column. + * + * @param string|array $relation + * @param string $column + * @return $this + */ + public function withMin($relation, $column) + { + return $this->withAggregate($relation, $column, 'min'); + } + + /** + * Add subselect queries to include the sum of the relation's column. + * + * @param string|array $relation + * @param string $column + * @return $this + */ + public function withSum($relation, $column) + { + return $this->withAggregate($relation, $column, 'sum'); + } + + /** + * Add subselect queries to include the average of the relation's column. + * + * @param string|array $relation + * @param string $column + * @return $this + */ + public function withAvg($relation, $column) + { + return $this->withAggregate($relation, $column, 'avg'); + } + /** * Add the "has" condition where clause to the query. * diff --git a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php index eb3e1ee41962..e0c42c4c642b 100644 --- a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php @@ -3,13 +3,14 @@ namespace Illuminate\Database\Eloquent\Factories; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; 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; @@ -30,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; @@ -50,7 +51,7 @@ public function __construct(Factory $factory, $pivot, $relationship) */ public function createFor(Model $model) { - $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 68793d407565..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,23 +52,26 @@ public function attributesFor(Model $model) $relationship = $model->{$this->relationship}(); return $relationship instanceof MorphTo ? [ - $relationship->getMorphType() => $this->factory->newModel()->getMorphClass(), - $relationship->getForeignKeyName() => $this->resolver(), + $relationship->getMorphType() => $this->factory instanceof Factory ? $this->factory->newModel()->getMorphClass() : $this->factory->getMorphClass(), + $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()), ] : [ - $relationship->getForeignKeyName() => $this->resolver(), + $relationship->getForeignKeyName() => $this->resolver($relationship->getOwnerKeyName()), ]; } /** * Get the deferred resolver for this relationship's parent ID. * + * @param string|null $key * @return \Closure */ - protected function resolver() + protected function resolver($key) { - return function () { + return function () use ($key) { if (! $this->resolved) { - return $this->resolved = $this->factory->create()->getKey(); + $instance = $this->factory instanceof Factory ? $this->factory->create() : $this->factory; + + return $this->resolved = $key ? $instance->{$key} : $instance->getKey(); } return $this->resolved; diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 665ef399efa2..36f3dad84a8a 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -7,9 +7,11 @@ use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Application; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; +use Throwable; abstract class Factory { @@ -103,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, @@ -177,7 +179,13 @@ public function configure() */ public function raw($attributes = [], ?Model $parent = null) { - return $this->state($attributes)->getExpandedAttributes($parent); + if ($this->count === null) { + return $this->state($attributes)->getExpandedAttributes($parent); + } + + return array_map(function () use ($attributes, $parent) { + return $this->state($attributes)->getExpandedAttributes($parent); + }, range(1, $this->count)); } /** @@ -234,6 +242,20 @@ public function create($attributes = [], ?Model $parent = null) return $results; } + /** + * Create a callback that persists a model in the database when invoked. + * + * @param array $attributes + * @param \Illuminate\Database\Eloquent\Model|null $parent + * @return \Closure + */ + public function lazy(array $attributes = [], ?Model $parent = null) + { + return function () use ($attributes, $parent) { + return $this->create($attributes, $parent); + }; + } + /** * Set the connection name on the results and store them. * @@ -459,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() + ))) )]), ]); } @@ -478,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 + )) )])]); } @@ -607,9 +635,11 @@ public function modelName() $resolver = static::$modelNameResolver ?: function (self $factory) { $factoryBasename = Str::replaceLast('Factory', '', class_basename($factory)); - return class_exists('App\\Models\\'.$factoryBasename) - ? 'App\\Models\\'.$factoryBasename - : 'App\\'.$factoryBasename; + $appNamespace = static::appNamespace(); + + return class_exists($appNamespace.'Models\\'.$factoryBasename) + ? $appNamespace.'Models\\'.$factoryBasename + : $appNamespace.$factoryBasename; }; return $this->model ?: $resolver($this); @@ -680,9 +710,11 @@ protected function withFaker() public static function resolveFactoryName(string $modelName) { $resolver = static::$factoryNameResolver ?: function (string $modelName) { - $modelName = Str::startsWith($modelName, 'App\\Models\\') - ? Str::after($modelName, 'App\\Models\\') - : Str::after($modelName, 'App\\'); + $appNamespace = static::appNamespace(); + + $modelName = Str::startsWith($modelName, $appNamespace.'Models\\') + ? Str::after($modelName, $appNamespace.'Models\\') + : Str::after($modelName, $appNamespace); return static::$namespace.$modelName.'Factory'; }; @@ -690,6 +722,22 @@ public static function resolveFactoryName(string $modelName) return $resolver($modelName); } + /** + * Get the application namespace for the application. + * + * @return string + */ + protected static function appNamespace() + { + try { + return Container::getInstance() + ->make(Application::class) + ->getNamespace(); + } catch (Throwable $e) { + return 'App\\'; + } + } + /** * Proxy dynamic factory methods onto their proper methods. * @@ -705,9 +753,13 @@ public function __call($method, $parameters) $relationship = Str::camel(Str::substr($method, 3)); - $factory = static::factoryForModel( - get_class($this->newModel()->{$relationship}()->getRelated()) - ); + $relatedModel = get_class($this->newModel()->{$relationship}()->getRelated()); + + if (method_exists($relatedModel, 'newFactory')) { + $factory = $relatedModel::newFactory() ?: static::factoryForModel($relatedModel); + } else { + $factory = static::factoryForModel($relatedModel); + } if (Str::startsWith($method, 'for')) { return $this->for($factory->state($parameters[0] ?? []), $relationship); diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 4df747e773dc..528f9ca497a0 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -146,14 +146,14 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab /** * The name of the "created at" column. * - * @var string + * @var string|null */ const CREATED_AT = 'created_at'; /** * The name of the "updated at" column. * - * @var string + * @var string|null */ const UPDATED_AT = 'updated_at'; @@ -516,6 +516,10 @@ public function load($relations) */ public function loadMorph($relation, $relations) { + if (! $this->{$relation}) { + return $this; + } + $className = get_class($this->{$relation}); $this->{$relation}->load($relations[$className] ?? []); @@ -538,6 +542,21 @@ public function loadMissing($relations) return $this; } + /** + * Eager load relation's column aggregations on the model. + * + * @param array|string $relations + * @param string $column + * @param string $function + * @return $this + */ + public function loadAggregate($relations, $column, $function = null) + { + $this->newCollection([$this])->loadAggregate($relations, $column, $function); + + return $this; + } + /** * Eager load relation counts on the model. * @@ -548,7 +567,75 @@ public function loadCount($relations) { $relations = is_string($relations) ? func_get_args() : $relations; - $this->newCollection([$this])->loadCount($relations); + return $this->loadAggregate($relations, '*', 'count'); + } + + /** + * Eager load relation max column values on the model. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadMax($relations, $column) + { + return $this->loadAggregate($relations, $column, 'max'); + } + + /** + * Eager load relation min column values on the model. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadMin($relations, $column) + { + return $this->loadAggregate($relations, $column, 'min'); + } + + /** + * Eager load relation's column summations on the model. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadSum($relations, $column) + { + return $this->loadAggregate($relations, $column, 'sum'); + } + + /** + * Eager load relation average column values on the model. + * + * @param array|string $relations + * @param string $column + * @return $this + */ + public function loadAvg($relations, $column) + { + return $this->loadAggregate($relations, $column, 'avg'); + } + + /** + * Eager load relationship column aggregation on the polymorphic relation of a model. + * + * @param string $relation + * @param array $relations + * @param string $column + * @param string $function + * @return $this + */ + public function loadMorphAggregate($relation, $relations, $column, $function = null) + { + if (! $this->{$relation}) { + return $this; + } + + $className = get_class($this->{$relation}); + + $this->{$relation}->loadAggregate($relations[$className] ?? [], $column, $function); return $this; } @@ -562,11 +649,59 @@ public function loadCount($relations) */ public function loadMorphCount($relation, $relations) { - $className = get_class($this->{$relation}); + return $this->loadMorphAggregate($relation, $relations, '*', 'count'); + } - $this->{$relation}->loadCount($relations[$className] ?? []); + /** + * Eager load relationship max column values on the polymorphic relation of a model. + * + * @param string $relation + * @param array $relations + * @param string $column + * @return $this + */ + public function loadMorphMax($relation, $relations, $column) + { + return $this->loadMorphAggregate($relation, $relations, $column, 'max'); + } - return $this; + /** + * Eager load relationship min column values on the polymorphic relation of a model. + * + * @param string $relation + * @param array $relations + * @param string $column + * @return $this + */ + public function loadMorphMin($relation, $relations, $column) + { + return $this->loadMorphAggregate($relation, $relations, $column, 'min'); + } + + /** + * Eager load relationship column summations on the polymorphic relation of a model. + * + * @param string $relation + * @param array $relations + * @param string $column + * @return $this + */ + public function loadMorphSum($relation, $relations, $column) + { + return $this->loadMorphAggregate($relation, $relations, $column, 'sum'); + } + + /** + * Eager load relationship average column values on the polymorphic relation of a model. + * + * @param string $relation + * @param array $relations + * @param string $column + * @return $this + */ + public function loadMorphAvg($relation, $relations, $column) + { + return $this->loadMorphAggregate($relation, $relations, $column, 'avg'); } /** @@ -612,7 +747,9 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) return $query->{$method}($column, $amount, $extra); } - $this->{$column} = $this->{$column} + ($method === 'increment' ? $amount : $amount * -1); + $this->{$column} = $this->isClassDeviable($column) + ? $this->deviateClassCastableAttribute($method, $column, $amount) + : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1); $this->forceFill($extra); @@ -620,9 +757,7 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) return false; } - return tap($query->where( - $this->getKeyName(), $this->getKey() - )->{$method}($column, $amount, $extra), function () use ($column) { + return tap($this->setKeysForSaveQuery($query)->{$method}($column, $amount, $extra), function () use ($column) { $this->syncChanges(); $this->fireModelEvent('updated', false); @@ -807,13 +942,36 @@ protected function performUpdate(Builder $query) return true; } + /** + * Set the keys for a select query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function setKeysForSelectQuery($query) + { + $query->where($this->getKeyName(), '=', $this->getKeyForSelectQuery()); + + return $query; + } + + /** + * Get the primary key value for a select query. + * + * @return mixed + */ + protected function getKeyForSelectQuery() + { + return $this->original[$this->getKeyName()] ?? $this->getKey(); + } + /** * Set the keys for a save update query. * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery(Builder $query) + protected function setKeysForSaveQuery($query) { $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); @@ -827,8 +985,7 @@ protected function setKeysForSaveQuery(Builder $query) */ protected function getKeyForSaveQuery() { - return $this->original[$this->getKeyName()] - ?? $this->getKey(); + return $this->original[$this->getKeyName()] ?? $this->getKey(); } /** @@ -904,22 +1061,23 @@ protected function insertAndSetId(Builder $query, $attributes) */ public static function destroy($ids) { - // We'll initialize a count here so we will return the total number of deletes - // for the operation. The developers can then check this number as a boolean - // type value or get this total count of records deleted for logging, etc. - $count = 0; - if ($ids instanceof BaseCollection) { $ids = $ids->all(); } $ids = is_array($ids) ? $ids : func_get_args(); + if (count($ids) === 0) { + return 0; + } + // We will actually pull the models from the database table and call delete on // each of them individually so that their events get fired properly with a // correct set of attributes in case the developers wants to check these. $key = ($instance = new static)->getKeyName(); + $count = 0; + foreach ($instance->whereIn($key, $ids)->get() as $model) { if ($model->delete()) { $count++; @@ -1209,9 +1367,8 @@ public function fresh($with = []) return; } - return static::newQueryWithoutScopes() + return $this->setKeysForSelectQuery($this->newQueryWithoutScopes()) ->with(is_string($with) ? func_get_args() : $with) - ->where($this->getKeyName(), $this->getKey()) ->first(); } @@ -1227,7 +1384,7 @@ public function refresh() } $this->setRawAttributes( - static::newQueryWithoutScopes()->findOrFail($this->getKey())->attributes + $this->setKeysForSelectQuery($this->newQueryWithoutScopes())->firstOrFail()->attributes ); $this->load(collect($this->relations)->reject(function ($relation) { diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php index 5ec2c315b8dc..ce7ba4421973 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php @@ -5,11 +5,12 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; class BelongsTo extends Relation { - use SupportsDefaultModels; + use ComparesRelatedModels, SupportsDefaultModels; /** * The child model instance of the relation. @@ -39,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. * @@ -228,6 +222,16 @@ public function dissociate() return $this->child->setRelation($this->relationName, null); } + /** + * Alias of "dissociate" method. + * + * @return \Illuminate\Database\Eloquent\Model + */ + public function disassociate() + { + return $this->dissociate(); + } + /** * Add the constraints for a relationship query. * @@ -268,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. * @@ -330,6 +324,16 @@ public function getQualifiedForeignKeyName() return $this->child->qualifyColumn($this->foreignKey); } + /** + * Get the key value of the child's foreign key. + * + * @return mixed + */ + public function getParentKey() + { + return $this->child->{$this->foreignKey}; + } + /** * Get the associated key of the relationship. * @@ -350,6 +354,17 @@ public function getQualifiedOwnerKeyName() return $this->related->qualifyColumn($this->ownerKey); } + /** + * Get the value of the model's associated key. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed + */ + protected function getRelatedKeyFrom(Model $model) + { + return $model->{$this->ownerKey}; + } + /** * Get the name of the relationship. * diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 97306dee1d3c..03a47861643c 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; use Illuminate\Support\Str; use InvalidArgumentException; @@ -126,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. * @@ -177,7 +171,7 @@ protected function resolveTableName($table) return $table; } - if ($model instanceof Pivot) { + if (in_array(AsPivot::class, class_uses_recursive($model))) { $this->using($table); } @@ -211,11 +205,12 @@ protected function performJoin($query = null) // We need to join to the intermediate table on the related model's primary // key column with the intermediate table's foreign key for the related // model instance. Then we can set the "where" for the parent models. - $baseTable = $this->related->getTable(); - - $key = $baseTable.'.'.$this->relatedKey; - - $query->join($this->table, $key, '=', $this->getQualifiedRelatedPivotKeyName()); + $query->join( + $this->table, + $this->getQualifiedRelatedKeyName(), + '=', + $this->getQualifiedRelatedPivotKeyName() + ); return $this; } @@ -361,7 +356,7 @@ public function wherePivot($column, $operator = null, $value = null, $boolean = { $this->pivotWheres[] = func_get_args(); - return $this->where($this->table.'.'.$column, $operator, $value, $boolean); + return $this->where($this->qualifyPivotColumn($column), $operator, $value, $boolean); } /** @@ -375,7 +370,7 @@ public function wherePivot($column, $operator = null, $value = null, $boolean = */ public function wherePivotBetween($column, array $values, $boolean = 'and', $not = false) { - return $this->whereBetween($this->table.'.'.$column, $values, $boolean, $not); + return $this->whereBetween($this->qualifyPivotColumn($column), $values, $boolean, $not); } /** @@ -428,7 +423,7 @@ public function wherePivotIn($column, $values, $boolean = 'and', $not = false) { $this->pivotWhereIns[] = func_get_args(); - return $this->whereIn($this->table.'.'.$column, $values, $boolean, $not); + return $this->whereIn($this->qualifyPivotColumn($column), $values, $boolean, $not); } /** @@ -523,7 +518,7 @@ public function wherePivotNull($column, $boolean = 'and', $not = false) { $this->pivotWhereNulls[] = func_get_args(); - return $this->whereNull($this->table.'.'.$column, $boolean, $not); + return $this->whereNull($this->qualifyPivotColumn($column), $boolean, $not); } /** @@ -561,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. * @@ -809,7 +816,7 @@ protected function aliasedPivotColumns() $defaults = [$this->foreignPivotKey, $this->relatedPivotKey]; return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { - return $this->table.'.'.$column.' as pivot_'.$column; + return $this->qualifyPivotColumn($column).' as pivot_'.$column; })->unique()->all(); } @@ -1165,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. * @@ -1229,7 +1226,7 @@ public function getForeignPivotKeyName() */ public function getQualifiedForeignPivotKeyName() { - return $this->table.'.'.$this->foreignPivotKey; + return $this->qualifyPivotColumn($this->foreignPivotKey); } /** @@ -1249,7 +1246,7 @@ public function getRelatedPivotKeyName() */ public function getQualifiedRelatedPivotKeyName() { - return $this->table.'.'.$this->relatedPivotKey; + return $this->qualifyPivotColumn($this->relatedPivotKey); } /** @@ -1282,6 +1279,16 @@ public function getRelatedKeyName() return $this->relatedKey; } + /** + * Get the fully qualified related key name for the relation. + * + * @return string + */ + public function getQualifiedRelatedKeyName() + { + return $this->related->qualifyColumn($this->relatedKey); + } + /** * Get the intermediate table for the relationship. * @@ -1321,4 +1328,17 @@ public function getPivotColumns() { return $this->pivotColumns; } + + /** + * Qualify the given column name by the pivot table. + * + * @param string $column + * @return string + */ + public function qualifyPivotColumn($column) + { + return Str::contains($column, '.') + ? $column + : $this->table.'.'.$column; + } } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php index a6fdd5af80b9..af9defb7463d 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php @@ -83,15 +83,15 @@ public static function fromRawAttributes(Model $parent, $attributes, $table, $ex } /** - * Set the keys for a save update query. + * Set the keys for a select query. * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery(Builder $query) + protected function setKeysForSelectQuery($query) { if (isset($this->attributes[$this->getKeyName()])) { - return parent::setKeysForSaveQuery($query); + return parent::setKeysForSelectQuery($query); } $query->where($this->foreignKey, $this->getOriginal( @@ -103,6 +103,17 @@ protected function setKeysForSaveQuery(Builder $query) )); } + /** + * Set the keys for a save update query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function setKeysForSaveQuery($query) + { + return $this->setKeysForSelectQuery($query); + } + /** * Delete the pivot model record from the database. * @@ -286,6 +297,8 @@ public function newQueryForRestoration($ids) */ protected function newQueryForCollectionRestoration(array $ids) { + $ids = array_values($ids); + if (! Str::contains($ids[0], ':')) { return parent::newQueryForRestoration($ids); } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php new file mode 100644 index 000000000000..50ec4f03e337 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -0,0 +1,68 @@ +compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) && + $this->related->getTable() === $model->getTable() && + $this->related->getConnectionName() === $model->getConnectionName(); + } + + /** + * Determine if the model is not the related instance of the relationship. + * + * @param \Illuminate\Database\Eloquent\Model|null $model + * @return bool + */ + public function isNot($model) + { + return ! $this->is($model); + } + + /** + * Get the value of the parent model's key. + * + * @return mixed + */ + abstract public function getParentKey(); + + /** + * Get the value of the model's related key. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed + */ + abstract protected function getRelatedKeyFrom(Model $model); + + /** + * Compare the parent key with the related key. + * + * @param mixed $parentKey + * @param mixed $relatedKey + * @return bool + */ + protected function compareKeys($parentKey, $relatedKey) + { + if (empty($parentKey) || empty($relatedKey)) { + return false; + } + + if (is_int($parentKey) || is_int($relatedKey)) { + return (int) $parentKey === (int) $relatedKey; + } + + return $parentKey === $relatedKey; + } +} diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 0d13eefc1469..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. * @@ -475,7 +490,7 @@ protected function detachUsingCustomClass($ids) protected function getCurrentlyAttachedPivots() { return $this->newPivotQuery()->get()->map(function ($record) { - $class = $this->using ? $this->using : Pivot::class; + $class = $this->using ?: Pivot::class; $pivot = $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true); @@ -541,15 +556,15 @@ public function newPivotQuery() $query = $this->newPivotStatement(); foreach ($this->pivotWheres as $arguments) { - call_user_func_array([$query, 'where'], $arguments); + $query->where(...$arguments); } foreach ($this->pivotWhereIns as $arguments) { - call_user_func_array([$query, 'whereIn'], $arguments); + $query->whereIn(...$arguments); } foreach ($this->pivotWhereNulls as $arguments) { - call_user_func_array([$query, 'whereNull'], $arguments); + $query->whereNull(...$arguments); } return $query->where($this->foreignPivotKey, $this->parent->{$this->parentKey}); 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/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index 1d9e008fd231..81ca9bb441cf 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -4,11 +4,12 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; class HasOne extends HasOneOrMany { - use SupportsDefaultModels; + use ComparesRelatedModels, SupportsDefaultModels; /** * Get the results of the relationship. @@ -65,4 +66,15 @@ public function newRelatedInstanceFor(Model $parent) $this->getForeignKeyName(), $parent->{$this->localKey} ); } + + /** + * Get the value of the model's foreign key. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed + */ + protected function getRelatedKeyFrom(Model $model) + { + return $model->getAttribute($this->getForeignKeyName()); + } } 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/MorphOne.php b/src/Illuminate/Database/Eloquent/Relations/MorphOne.php index 5f8da14f1f46..a874cdaec8d6 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOne.php @@ -4,11 +4,12 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; class MorphOne extends MorphOneOrMany { - use SupportsDefaultModels; + use ComparesRelatedModels, SupportsDefaultModels; /** * Get the results of the relationship. @@ -65,4 +66,15 @@ public function newRelatedInstanceFor(Model $parent) ->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}) ->setAttribute($this->getMorphType(), $this->morphClass); } + + /** + * Get the value of the model's foreign key. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed + */ + protected function getRelatedKeyFrom(Model $model) + { + return $model->getAttribute($this->getForeignKeyName()); + } } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php index 0db82ba101bc..314d61fc7f36 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php @@ -2,7 +2,6 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Str; class MorphPivot extends Pivot @@ -31,13 +30,26 @@ class MorphPivot extends Pivot * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery(Builder $query) + protected function setKeysForSaveQuery($query) { $query->where($this->morphType, $this->morphClass); return parent::setKeysForSaveQuery($query); } + /** + * Set the keys for a select query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function setKeysForSelectQuery($query) + { + $query->where($this->morphType, $this->morphClass); + + return parent::setKeysForSelectQuery($query); + } + /** * Delete the pivot model record from the database. * @@ -139,6 +151,8 @@ public function newQueryForRestoration($ids) */ protected function newQueryForCollectionRestoration(array $ids) { + $ids = array_values($ids); + if (! Str::contains($ids[0], ':')) { return parent::newQueryForRestoration($ids); } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index 22d1d4d2c1a2..c9db5d2b2c6c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -51,6 +51,13 @@ class MorphTo extends BelongsTo */ protected $morphableEagerLoadCounts = []; + /** + * A map of constraints to apply for each individual morph type. + * + * @var array + */ + protected $morphableConstraints = []; + /** * Create a new morph to relationship instance. * @@ -133,10 +140,14 @@ protected function getResultsByType($type) (array) ($this->morphableEagerLoadCounts[get_class($instance)] ?? []) ); + if ($callback = ($this->morphableConstraints[get_class($instance)] ?? null)) { + $callback($query); + } + $whereIn = $this->whereInMethod($instance, $ownerKey); return $query->{$whereIn}( - $instance->getTable().'.'.$ownerKey, $this->gatherKeysByType($type) + $instance->getTable().'.'.$ownerKey, $this->gatherKeysByType($type, $instance->getKeyType()) )->get(); } @@ -144,11 +155,16 @@ protected function getResultsByType($type) * Gather all of the foreign keys for a given type. * * @param string $type + * @param string $keyType * @return array */ - protected function gatherKeysByType($type) + protected function gatherKeysByType($type, $keyType) { - return array_keys($this->dictionary[$type]); + return $keyType !== 'string' + ? array_keys($this->dictionary[$type]) + : array_map(function ($modelId) { + return (string) $modelId; + }, array_keys($this->dictionary[$type])); } /** @@ -307,6 +323,21 @@ public function morphWithCount(array $withCount) return $this; } + /** + * Specify constraints on the query for a given morph types. + * + * @param array $callbacks + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function constrain(array $callbacks) + { + $this->morphableConstraints = array_merge( + $this->morphableConstraints, $callbacks + ); + + return $this; + } + /** * Replay stored macro calls on the actual related instance. * diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 0adf385e13d6..c2d574558224 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -68,7 +68,7 @@ protected function addWhereConstraints() { parent::addWhereConstraints(); - $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); + $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); return $this; } @@ -83,7 +83,7 @@ public function addEagerConstraints(array $models) { parent::addEagerConstraints($models); - $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); + $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); } /** @@ -111,7 +111,7 @@ protected function baseAttachRecord($id, $timed) public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( - $this->table.'.'.$this->morphType, $this->morphClass + $this->qualifyPivotColumn($this->morphType), $this->morphClass ); } @@ -173,7 +173,7 @@ protected function aliasedPivotColumns() $defaults = [$this->foreignPivotKey, $this->relatedPivotKey, $this->morphType]; return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { - return $this->table.'.'.$column.' as pivot_'.$column; + return $this->qualifyPivotColumn($column).' as pivot_'.$column; })->unique()->all(); } diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 6bdb6f7a7487..3fcb75e67ab5 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -55,6 +55,13 @@ abstract class Relation */ public static $morphMap = []; + /** + * The count of self joins. + * + * @var int + */ + protected static $selfJoinCount = 0; + /** * Create a new relation instance. * @@ -213,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. * diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index 0d5169662490..7528964c132a 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -7,7 +7,7 @@ class SoftDeletingScope implements Scope /** * All of the extensions to be added to the builder. * - * @var array + * @var string[] */ protected $extensions = ['Restore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed']; diff --git a/src/Illuminate/Database/Events/DatabaseRefreshed.php b/src/Illuminate/Database/Events/DatabaseRefreshed.php new file mode 100644 index 000000000000..5b1fb45856b3 --- /dev/null +++ b/src/Illuminate/Database/Events/DatabaseRefreshed.php @@ -0,0 +1,10 @@ +{"register{$command}Command"}(); } $this->commands(array_values($commands)); diff --git a/src/Illuminate/Database/MySqlConnection.php b/src/Illuminate/Database/MySqlConnection.php index b0de7840270a..9760358cf5f4 100755 --- a/src/Illuminate/Database/MySqlConnection.php +++ b/src/Illuminate/Database/MySqlConnection.php @@ -3,6 +3,8 @@ namespace Illuminate\Database; use Doctrine\DBAL\Driver\PDOMySql\Driver as DoctrineDriver; +use Doctrine\DBAL\Version; +use Illuminate\Database\PDO\MySqlDriver; use Illuminate\Database\Query\Grammars\MySqlGrammar as QueryGrammar; use Illuminate\Database\Query\Processors\MySqlProcessor; use Illuminate\Database\Schema\Grammars\MySqlGrammar as SchemaGrammar; @@ -82,10 +84,10 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOMySql\Driver + * @return \Doctrine\DBAL\Driver\PDOMySql\Driver|\Illuminate\Database\PDO\MySqlDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return class_exists(Version::class) ? new DoctrineDriver : new MySqlDriver; } } diff --git a/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php b/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php new file mode 100644 index 000000000000..637c62ce1f5d --- /dev/null +++ b/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php @@ -0,0 +1,24 @@ +connection = $connection; + } + + /** + * Execute an SQL statement. + * + * @param string $statement + * @return int + */ + public function exec(string $statement): int + { + try { + $result = $this->connection->exec($statement); + + \assert($result !== false); + + return $result; + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * Prepare a new SQL statement. + * + * @param string $sql + * @return \Doctrine\DBAL\Driver\Statement + */ + public function prepare(string $sql): StatementInterface + { + try { + return $this->createStatement( + $this->connection->prepare($sql) + ); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * Execute a new query against the connection. + * + * @param string $sql + * @return \Doctrine\DBAL\Driver\Result + */ + public function query(string $sql): ResultInterface + { + try { + $stmt = $this->connection->query($sql); + + \assert($stmt instanceof PDOStatement); + + return new Result($stmt); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * Get the last insert ID. + * + * @param string|null $name + * @return mixed + */ + public function lastInsertId($name = null) + { + try { + if ($name === null) { + return $this->connection->lastInsertId(); + } + + return $this->connection->lastInsertId($name); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * Create a new statement instance. + * + * @param \PDOStatement + * @return \Doctrine\DBAL\Driver\PDO\Statement + */ + protected function createStatement(PDOStatement $stmt): Statement + { + return new Statement($stmt); + } + + /** + * Begin a new database transaction. + * + * @return void + */ + public function beginTransaction() + { + return $this->connection->beginTransaction(); + } + + /** + * Commit a database transaction. + * + * @return void + */ + public function commit() + { + return $this->connection->commit(); + } + + /** + * Roll back a database transaction. + * + * @return void + */ + public function rollBack() + { + return $this->connection->rollBack(); + } + + /** + * Wrap quotes around the given input. + * + * @param string $input + * @param string $type + * @return string + */ + public function quote($input, $type = ParameterType::STRING) + { + return $this->connection->quote($input, $type); + } + + /** + * Get the server version for the connection. + * + * @return string + */ + public function getServerVersion() + { + return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Get the wrapped PDO connection. + * + * @return \PDO + */ + public function getWrappedConnection(): PDO + { + return $this->connection; + } +} diff --git a/src/Illuminate/Database/PDO/MySqlDriver.php b/src/Illuminate/Database/PDO/MySqlDriver.php new file mode 100644 index 000000000000..5f68c6fab5a4 --- /dev/null +++ b/src/Illuminate/Database/PDO/MySqlDriver.php @@ -0,0 +1,11 @@ +connection = $connection; + } + + /** + * Prepare a new SQL statement. + * + * @param string $sql + * @return \Doctrine\DBAL\Driver\Statement + */ + public function prepare(string $sql): StatementInterface + { + return new Statement( + $this->connection->prepare($sql) + ); + } + + /** + * Execute a new query against the connection. + * + * @param string $sql + * @return \Doctrine\DBAL\Driver\Result + */ + public function query(string $sql): Result + { + return $this->connection->query($sql); + } + + /** + * Execute an SQL statement. + * + * @param string $statement + * @return int + */ + public function exec(string $statement): int + { + return $this->connection->exec($statement); + } + + /** + * Get the last insert ID. + * + * @param string|null $name + * @return mixed + */ + public function lastInsertId($name = null) + { + if ($name === null) { + return $this->connection->lastInsertId($name); + } + + return $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?') + ->execute([$name]) + ->fetchOne(); + } + + /** + * Begin a new database transaction. + * + * @return void + */ + public function beginTransaction() + { + return $this->connection->beginTransaction(); + } + + /** + * Commit a database transaction. + * + * @return void + */ + public function commit() + { + return $this->connection->commit(); + } + + /** + * Roll back a database transaction. + * + * @return void + */ + public function rollBack() + { + return $this->connection->rollBack(); + } + + /** + * Wrap quotes around the given input. + * + * @param string $value + * @param int $type + * @return string + */ + public function quote($value, $type = ParameterType::STRING) + { + $val = $this->connection->quote($value, $type); + + // Fix for a driver version terminating all values with null byte... + if (\is_string($val) && \strpos($val, "\0") !== false) { + $val = \substr($val, 0, -1); + } + + return $val; + } + + /** + * Get the server version for the connection. + * + * @return string + */ + public function getServerVersion() + { + return $this->connection->getServerVersion(); + } + + /** + * Get the wrapped PDO connection. + * + * @return \PDO + */ + public function getWrappedConnection(): PDO + { + return $this->connection->getWrappedConnection(); + } +} diff --git a/src/Illuminate/Database/PDO/SqlServerDriver.php b/src/Illuminate/Database/PDO/SqlServerDriver.php new file mode 100644 index 000000000000..bbb3bbd32397 --- /dev/null +++ b/src/Illuminate/Database/PDO/SqlServerDriver.php @@ -0,0 +1,15 @@ +', '<=', '>=', '<>', '!=', '<=>', @@ -243,7 +244,7 @@ public function select($columns = ['*']) /** * Add a subselect expression to the query. * - * @param \Closure|$this|string $query + * @param \Closure|\Illuminate\Database\Query\Builder|string $query * @param string $as * @return $this * @@ -751,7 +752,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' ); if (! $value instanceof Expression) { - $this->addBinding($value, 'where'); + $this->addBinding(is_array($value) ? head($value) : $value, 'where'); } return $this; @@ -1120,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($values), 0, 2), 'where'); return $this; } @@ -1243,6 +1244,8 @@ public function whereDate($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = is_array($value) ? head($value) : $value; + if ($value instanceof DateTimeInterface) { $value = $value->format('Y-m-d'); } @@ -1282,6 +1285,8 @@ public function whereTime($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = is_array($value) ? head($value) : $value; + if ($value instanceof DateTimeInterface) { $value = $value->format('H:i:s'); } @@ -1321,6 +1326,8 @@ public function whereDay($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = is_array($value) ? head($value) : $value; + if ($value instanceof DateTimeInterface) { $value = $value->format('d'); } @@ -1364,6 +1371,8 @@ public function whereMonth($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = is_array($value) ? head($value) : $value; + if ($value instanceof DateTimeInterface) { $value = $value->format('m'); } @@ -1407,6 +1416,8 @@ public function whereYear($column, $operator, $value = null, $boolean = 'and') $value, $operator, func_num_args() === 2 ); + $value = is_array($value) ? head($value) : $value; + if ($value instanceof DateTimeInterface) { $value = $value->format('Y'); } @@ -1715,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) $value); } return $this; @@ -1864,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(is_array($value) ? head($value) : $value, 'having'); } return $this; @@ -2923,6 +2934,49 @@ public function updateOrInsert(array $attributes, array $values = []) return (bool) $this->limit(1)->update($values); } + /** + * Insert new records or update the existing ones. + * + * @param array $values + * @param array|string $uniqueBy + * @param array|null $update + * @return int + */ + public function upsert(array $values, $uniqueBy, $update = null) + { + if (empty($values)) { + return 0; + } elseif ($update === []) { + return (int) $this->insert($values); + } + + if (! is_array(reset($values))) { + $values = [$values]; + } else { + foreach ($values as $key => $value) { + ksort($value); + + $values[$key] = $value; + } + } + + if (is_null($update)) { + $update = array_keys(reset($values)); + } + + $bindings = $this->cleanBindings(array_merge( + Arr::flatten($values, 1), + collect($update)->reject(function ($value, $key) { + return is_int($key); + })->all() + )); + + return $this->connection->affectingStatement( + $this->grammar->compileUpsert($this, $values, (array) $uniqueBy, $update), + $bindings + ); + } + /** * Increment a column's value by a given amount. * @@ -3190,6 +3244,16 @@ protected function isQueryable($value) $value instanceof Closure; } + /** + * Clone the query. + * + * @return static + */ + public function clone() + { + return clone $this; + } + /** * Clone the query without the given properties. * @@ -3198,7 +3262,7 @@ protected function isQueryable($value) */ public function cloneWithout(array $properties) { - return tap(clone $this, function ($clone) use ($properties) { + return tap($this->clone(), function ($clone) use ($properties) { foreach ($properties as $property) { $clone->{$property} = null; } @@ -3213,7 +3277,7 @@ public function cloneWithout(array $properties) */ public function cloneWithoutBindings(array $except) { - return tap(clone $this, function ($clone) use ($except) { + return tap($this->clone(), function ($clone) use ($except) { foreach ($except as $type) { $clone->bindings[$type] = []; } diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 1cc2562a2cab..8b0a3b311b78 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -21,7 +21,7 @@ class Grammar extends BaseGrammar /** * The components that make up a select clause. * - * @var array + * @var string[] */ protected $selectComponents = [ 'aggregate', @@ -995,6 +995,22 @@ protected function compileUpdateWithJoins(Builder $query, $table, $columns, $whe return "update {$table} {$joins} set {$columns} {$where}"; } + /** + * Compile an "upsert" statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param array $uniqueBy + * @param array $update + * @return string + * + * @throws \RuntimeException + */ + public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) + { + throw new RuntimeException('This database engine does not support upserts.'); + } + /** * Prepare the bindings for an update statement. * diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 494018803723..17b1aff01347 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -10,17 +10,16 @@ class MySqlGrammar extends Grammar /** * The grammar specific operators. * - * @var array + * @var string[] */ protected $operators = ['sounds like']; /** * 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) { @@ -153,6 +152,28 @@ protected function compileUpdateColumns(Builder $query, array $values) })->implode(', '); } + /** + * Compile an "upsert" statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param array $uniqueBy + * @param array $update + * @return string + */ + public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) + { + $sql = $this->compileInsert($query, $values).' on duplicate key update '; + + $columns = collect($update)->map(function ($value, $key) { + return is_numeric($key) + ? $this->wrap($value).' = values('.$this->wrap($value).')' + : $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); + + return $sql.$columns; + } + /** * Prepare a JSON column being updated using the JSON_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index a5c1368effbb..f0896a35a3b2 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -11,7 +11,7 @@ class PostgresGrammar extends Grammar /** * All of the available clause operators. * - * @var array + * @var string[] */ protected $operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', @@ -218,6 +218,30 @@ protected function compileUpdateColumns(Builder $query, array $values) })->implode(', '); } + /** + * Compile an "upsert" statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param array $uniqueBy + * @param array $update + * @return string + */ + public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) + { + $sql = $this->compileInsert($query, $values); + + $sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set '; + + $columns = collect($update)->map(function ($value, $key) { + return is_numeric($key) + ? $this->wrap($value).' = '.$this->wrapValue('excluded').'.'.$this->wrap($value) + : $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); + + return $sql.$columns; + } + /** * Prepares a JSON column being updated using the JSONB_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 2c27ddf3c0e6..29a3796860e7 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -11,7 +11,7 @@ class SQLiteGrammar extends Grammar /** * All of the available clause operators. * - * @var array + * @var string[] */ protected $operators = [ '=', '<', '>', '<=', '>=', '<>', '!=', @@ -182,6 +182,30 @@ protected function compileUpdateColumns(Builder $query, array $values) })->implode(', '); } + /** + * Compile an "upsert" statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param array $uniqueBy + * @param array $update + * @return string + */ + public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) + { + $sql = $this->compileInsert($query, $values); + + $sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set '; + + $columns = collect($update)->map(function ($value, $key) { + return is_numeric($key) + ? $this->wrap($value).' = '.$this->wrapValue('excluded').'.'.$this->wrap($value) + : $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); + + return $sql.$columns; + } + /** * Group the nested JSON columns. * diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index 9dfc22939446..6bdaf668c25f 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -11,7 +11,7 @@ class SqlServerGrammar extends Grammar /** * All of the available clause operators. * - * @var array + * @var string[] */ protected $operators = [ '=', '<', '>', '<=', '>=', '!<', '!>', '<>', '!=', @@ -341,6 +341,48 @@ protected function compileUpdateWithJoins(Builder $query, $table, $columns, $whe return "update {$alias} set {$columns} from {$table} {$joins} {$where}"; } + /** + * Compile an "upsert" statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @param array $uniqueBy + * @param array $update + * @return string + */ + public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) + { + $columns = $this->columnize(array_keys(reset($values))); + + $sql = 'merge '.$this->wrapTable($query->from).' '; + + $parameters = collect($values)->map(function ($record) { + return '('.$this->parameterize($record).')'; + })->implode(', '); + + $sql .= 'using (values '.$parameters.') '.$this->wrapTable('laravel_source').' ('.$columns.') '; + + $on = collect($uniqueBy)->map(function ($column) use ($query) { + return $this->wrap('laravel_source.'.$column).' = '.$this->wrap($query->from.'.'.$column); + })->implode(' and '); + + $sql .= 'on '.$on.' '; + + if ($update) { + $update = collect($update)->map(function ($value, $key) { + return is_numeric($key) + ? $this->wrap($value).' = '.$this->wrap('laravel_source.'.$value) + : $this->wrap($key).' = '.$this->parameter($value); + })->implode(', '); + + $sql .= 'when matched then update set '.$update.' '; + } + + $sql .= 'when not matched then insert ('.$columns.') values ('.$columns.');'; + + return $sql; + } + /** * Prepare the bindings for an update statement. * diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index e647b13a625c..38116877c3ca 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -3,6 +3,8 @@ namespace Illuminate\Database; use Doctrine\DBAL\Driver\PDOSqlite\Driver as DoctrineDriver; +use Doctrine\DBAL\Version; +use Illuminate\Database\PDO\SQLiteDriver; use Illuminate\Database\Query\Grammars\SQLiteGrammar as QueryGrammar; use Illuminate\Database\Query\Processors\SQLiteProcessor; use Illuminate\Database\Schema\Grammars\SQLiteGrammar as SchemaGrammar; @@ -96,11 +98,11 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOSqlite\Driver + * @return \Doctrine\DBAL\Driver\PDOSqlite\Driver|\Illuminate\Database\PDO\SQLiteDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return class_exists(Version::class) ? new DoctrineDriver : new SQLiteDriver; } /** diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index e22bb4980d19..36dfbb7597b6 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -5,7 +5,6 @@ use BadMethodCallException; use Closure; use Illuminate\Database\Connection; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Grammars\Grammar; use Illuminate\Database\SQLiteConnection; @@ -383,6 +382,19 @@ public function dropForeign($index) return $this->dropIndexCommand('dropForeign', 'foreign', $index); } + /** + * Indicate that the given column and foreign key should be dropped. + * + * @param string $column + * @return \Illuminate\Support\Fluent + */ + public function dropConstrainedForeignId($column) + { + $this->dropForeign([$column]); + + return $this->dropColumn($column); + } + /** * Indicate that the given indexes should be renamed. * @@ -847,7 +859,7 @@ public function foreignIdFor($model, $column = null) $model = new $model; } - return $model->getKeyType() === 'int' && $model->incrementing + return $model->getKeyType() === 'int' && $model->getIncrementing() ? $this->foreignId($column ?: $model->getForeignKey()) : $this->foreignUuid($column ?: $model->getForeignKey()); } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index ca1455f63f3c..80611bfc92ed 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -226,6 +226,20 @@ public function dropIfExists($table) })); } + /** + * Drop columns from a table schema. + * + * @param string $table + * @param string|array $columns + * @return void + */ + public function dropColumns($table, $columns) + { + $this->table($table, function (Blueprint $blueprint) use ($columns) { + $blueprint->dropColumn($columns); + }); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/Schema/ColumnDefinition.php b/src/Illuminate/Database/Schema/ColumnDefinition.php index 4bc047fbf0d4..6b1e4f11f839 100644 --- a/src/Illuminate/Database/Schema/ColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ColumnDefinition.php @@ -25,6 +25,7 @@ * @method $this unique(string $indexName = null) Add a unique index * @method $this unsigned() Set the INTEGER column as UNSIGNED (MySQL) * @method $this useCurrent() Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value + * @method $this useCurrentOnUpdate() Set the TIMESTAMP column to use CURRENT_TIMESTAMP when updating (MySQL) * @method $this virtualAs(string $expression) Create a virtual generated column (MySQL/SQLite) */ class ColumnDefinition extends Fluent diff --git a/src/Illuminate/Database/Schema/ForeignKeyDefinition.php b/src/Illuminate/Database/Schema/ForeignKeyDefinition.php index 6fe970608fca..0354cc924678 100644 --- a/src/Illuminate/Database/Schema/ForeignKeyDefinition.php +++ b/src/Illuminate/Database/Schema/ForeignKeyDefinition.php @@ -34,6 +34,16 @@ public function cascadeOnDelete() return $this->onDelete('cascade'); } + /** + * Indicate that deletes should be restricted. + * + * @return $this + */ + public function restrictOnDelete() + { + return $this->onDelete('restrict'); + } + /** * Indicate that deletes should set the foreign key value to null. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 9d7a955b78a2..c2952e47926c 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -12,7 +12,7 @@ class MySqlGrammar extends Grammar /** * The possible column modifiers. * - * @var array + * @var string[] */ protected $modifiers = [ 'Unsigned', 'Charset', 'Collate', 'VirtualAs', 'StoredAs', 'Nullable', @@ -22,7 +22,7 @@ class MySqlGrammar extends Grammar /** * The possible column serials. * - * @var array + * @var string[] */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; @@ -706,9 +706,11 @@ protected function typeTimestamp(Fluent $column) { $columnType = $column->precision ? "timestamp($column->precision)" : 'timestamp'; - $defaultCurrent = $column->precision ? "CURRENT_TIMESTAMP($column->precision)" : 'CURRENT_TIMESTAMP'; + $current = $column->precision ? "CURRENT_TIMESTAMP($column->precision)" : 'CURRENT_TIMESTAMP'; - return $column->useCurrent ? "$columnType default $defaultCurrent" : $columnType; + $columnType = $column->useCurrent ? "$columnType default $current" : $columnType; + + return $column->useCurrentOnUpdate ? "$columnType on update $current" : $columnType; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index aa48d024ee93..204beecea3b2 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -17,21 +17,21 @@ class PostgresGrammar extends Grammar /** * The possible column modifiers. * - * @var array + * @var string[] */ protected $modifiers = ['Collate', 'Increment', 'Nullable', 'Default', 'VirtualAs', 'StoredAs']; /** * The columns available as serials. * - * @var array + * @var string[] */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; /** * The commands to be executed outside of create or alter command. * - * @var array + * @var string[] */ protected $fluentCommands = ['Comment']; diff --git a/src/Illuminate/Database/Schema/Grammars/RenameColumn.php b/src/Illuminate/Database/Schema/Grammars/RenameColumn.php index 777c9d897b66..0db0c507e404 100644 --- a/src/Illuminate/Database/Schema/Grammars/RenameColumn.php +++ b/src/Illuminate/Database/Schema/Grammars/RenameColumn.php @@ -63,9 +63,22 @@ protected static function getRenamedDiff(Grammar $grammar, Blueprint $blueprint, protected static function setRenamedColumns(TableDiff $tableDiff, Fluent $command, Column $column) { $tableDiff->renamedColumns = [ - $command->from => new Column($command->to, $column->getType(), $column->toArray()), + $command->from => new Column($command->to, $column->getType(), self::getWritableColumnOptions($column)), ]; return $tableDiff; } + + /** + * Get the writable column options. + * + * @param \Doctrine\DBAL\Schema\Column $column + * @return array + */ + private static function getWritableColumnOptions(Column $column) + { + return array_filter($column->toArray(), function (string $name) use ($column) { + return method_exists($column, 'set'.$name); + }, ARRAY_FILTER_USE_KEY); + } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 5a154c226c97..556d749e23b2 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -14,14 +14,14 @@ class SQLiteGrammar extends Grammar /** * The possible column modifiers. * - * @var array + * @var string[] */ protected $modifiers = ['VirtualAs', 'StoredAs', 'Nullable', 'Default', 'Increment']; /** * The columns available as serials. * - * @var array + * @var string[] */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; @@ -875,7 +875,7 @@ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column) protected function modifyNullable(Blueprint $blueprint, Fluent $column) { if (is_null($column->virtualAs) && is_null($column->storedAs)) { - return $column->nullable ? ' null' : ' not null'; + return $column->nullable ? '' : ' not null'; } if ($column->nullable === false) { diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 43d3b7d0568d..79f3f0f5e3ad 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -17,14 +17,14 @@ class SqlServerGrammar extends Grammar /** * The possible column modifiers. * - * @var array + * @var string[] */ protected $modifiers = ['Increment', 'Collate', 'Nullable', 'Default', 'Persisted']; /** * The columns available as serials. * - * @var array + * @var string[] */ protected $serials = ['tinyInteger', 'smallInteger', 'mediumInteger', 'integer', 'bigInteger']; diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 3cb7dc93b250..2d46ebe32355 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Schema; use Exception; +use Illuminate\Database\Connection; use Illuminate\Support\Str; use Symfony\Component\Process\Process; @@ -11,10 +12,11 @@ class MySqlSchemaState extends SchemaState /** * Dump the database's schema into a file. * + * @param \Illuminate\Database\Connection $connection * @param string $path * @return void */ - public function dump($path) + public function dump(Connection $connection, $path) { $this->executeDumpProcess($this->makeProcess( $this->baseDumpCommand().' --routines --result-file="${:LARAVEL_LOAD_PATH}" --no-data' @@ -51,7 +53,7 @@ protected function removeAutoIncrementingState(string $path) protected function appendMigrationData(string $path) { $process = $this->executeDumpProcess($this->makeProcess( - $this->baseDumpCommand().' migrations --no-create-info --skip-extended-insert --skip-routines --compact' + $this->baseDumpCommand().' '.$this->migrationTable.' --no-create-info --skip-extended-insert --skip-routines --compact' ), null, array_merge($this->baseVariables($this->connection->getConfig()), [ // ])); @@ -67,9 +69,9 @@ protected function appendMigrationData(string $path) */ public function load($path) { - $process = $this->makeProcess('mysql --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"'); + $command = 'mysql '.$this->connectionString().' --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"'; - $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + $this->makeProcess($command)->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, ])); } @@ -81,11 +83,29 @@ public function load($path) */ protected function baseDumpCommand() { - $columnStatistics = $this->connection->isMaria() ? '' : '--column-statistics=0'; + $command = 'mysqldump '.$this->connectionString().' --skip-add-locks --skip-comments --skip-set-charset --tz-utc'; - $gtidPurged = $this->connection->isMaria() ? '' : '--set-gtid-purged=OFF'; + if (! $this->connection->isMaria()) { + $command .= ' --column-statistics=0 --set-gtid-purged=OFF'; + } - return 'mysqldump '.$gtidPurged.' '.$columnStatistics.' --skip-add-drop-table --skip-add-locks --skip-comments --skip-set-charset --tz-utc --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" "${:LARAVEL_LOAD_DATABASE}"'; + return $command.' "${:LARAVEL_LOAD_DATABASE}"'; + } + + /** + * Generate a basic connection string (--socket, --host, --port, --user, --password) for the database. + * + * @return string + */ + protected function connectionString() + { + $value = ' --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}"'; + + $value .= $this->connection->getConfig()['unix_socket'] ?? false + ? ' --socket="${:LARAVEL_LOAD_SOCKET}"' + : ' --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}"'; + + return $value; } /** @@ -96,9 +116,12 @@ protected function baseDumpCommand() */ protected function baseVariables(array $config) { + $config['host'] = $config['host'] ?? ''; + return [ + 'LARAVEL_LOAD_SOCKET' => $config['unix_socket'] ?? '', 'LARAVEL_LOAD_HOST' => is_array($config['host']) ? $config['host'][0] : $config['host'], - 'LARAVEL_LOAD_PORT' => $config['port'], + 'LARAVEL_LOAD_PORT' => $config['port'] ?? '', 'LARAVEL_LOAD_USER' => $config['username'], 'LARAVEL_LOAD_PASSWORD' => $config['password'], 'LARAVEL_LOAD_DATABASE' => $config['database'], @@ -116,7 +139,7 @@ protected function baseVariables(array $config) protected function executeDumpProcess(Process $process, $output, array $variables) { try { - $process->mustRun($output, $variables); + $process->setTimeout(null)->mustRun($output, $variables); } catch (Exception $e) { if (Str::contains($e->getMessage(), ['column-statistics', 'column_statistics'])) { return $this->executeDumpProcess(Process::fromShellCommandLine( diff --git a/src/Illuminate/Database/Schema/PostgresSchemaState.php b/src/Illuminate/Database/Schema/PostgresSchemaState.php index 81fef2632a8a..179db6189c55 100644 --- a/src/Illuminate/Database/Schema/PostgresSchemaState.php +++ b/src/Illuminate/Database/Schema/PostgresSchemaState.php @@ -2,45 +2,33 @@ namespace Illuminate\Database\Schema; +use Illuminate\Database\Connection; +use Illuminate\Support\Str; + class PostgresSchemaState extends SchemaState { /** * Dump the database's schema into a file. * + * @param \Illuminate\Database\Connection $connection * @param string $path * @return void */ - public function dump($path) + public function dump(Connection $connection, $path) { + $excludedTables = collect($connection->getSchemaBuilder()->getAllTables()) + ->map->tablename + ->reject(function ($table) { + return $table === $this->migrationTable; + })->map(function ($table) { + return '--exclude-table-data='.$table; + })->implode(' '); + $this->makeProcess( - $this->baseDumpCommand().' --no-owner --file=$LARAVEL_LOAD_PATH --schema-only' + $this->baseDumpCommand().' --file=$LARAVEL_LOAD_PATH '.$excludedTables )->mustRun($this->output, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, ])); - - $this->appendMigrationData($path); - } - - /** - * Append the migration data to the schema dump. - * - * @param string $path - * @return void - */ - protected function appendMigrationData(string $path) - { - with($process = $this->makeProcess( - $this->baseDumpCommand().' --table=migrations --data-only --inserts' - ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - // - ])); - - $migrations = collect(preg_split("/\r\n|\n|\r/", $process->getOutput()))->filter(function ($line) { - return preg_match('/^\s*(--|SELECT\s|SET\s)/iu', $line) === 0 && - strlen($line) > 0; - })->all(); - - $this->files->append($path, implode(PHP_EOL, $migrations).PHP_EOL); } /** @@ -51,7 +39,13 @@ protected function appendMigrationData(string $path) */ public function load($path) { - $process = $this->makeProcess('PGPASSWORD=$LARAVEL_LOAD_PASSWORD psql --file=$LARAVEL_LOAD_PATH --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE'); + $command = 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_restore --no-owner --no-acl --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE $LARAVEL_LOAD_PATH'; + + if (Str::endsWith($path, '.sql')) { + $command = 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD psql --file=$LARAVEL_LOAD_PATH --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER --dbname=$LARAVEL_LOAD_DATABASE'; + } + + $process = $this->makeProcess($command); $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, @@ -65,7 +59,7 @@ public function load($path) */ protected function baseDumpCommand() { - return 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_dump --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER $LARAVEL_LOAD_DATABASE'; + return 'PGPASSWORD=$LARAVEL_LOAD_PASSWORD pg_dump --no-owner --no-acl -Fc --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --username=$LARAVEL_LOAD_USER $LARAVEL_LOAD_DATABASE'; } /** diff --git a/src/Illuminate/Database/Schema/SchemaState.php b/src/Illuminate/Database/Schema/SchemaState.php index 072ffb8cbe23..5629a7aa303e 100644 --- a/src/Illuminate/Database/Schema/SchemaState.php +++ b/src/Illuminate/Database/Schema/SchemaState.php @@ -22,6 +22,13 @@ abstract class SchemaState */ protected $files; + /** + * The name of the application's migration table. + * + * @var string + */ + protected $migrationTable = 'migrations'; + /** * The process factory callback. * @@ -40,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) @@ -62,10 +69,11 @@ public function __construct(Connection $connection, Filesystem $files = null, ca /** * Dump the database's schema into a file. * + * @param \Illuminate\Database\Connection $connection * @param string $path * @return void */ - abstract public function dump($path); + abstract public function dump(Connection $connection, $path); /** * Load the given schema file into the database. @@ -86,6 +94,19 @@ public function makeProcess(...$arguments) return call_user_func($this->processFactory, ...$arguments); } + /** + * Specify the name of the application's migration table. + * + * @param string $table + * @return $this + */ + public function withMigrationTable(string $table) + { + $this->migrationTable = $table; + + return $this; + } + /** * Specify the callback that should be used to handle process output. * diff --git a/src/Illuminate/Database/Schema/SqliteSchemaState.php b/src/Illuminate/Database/Schema/SqliteSchemaState.php index 773affb2ccf2..42652baa420d 100644 --- a/src/Illuminate/Database/Schema/SqliteSchemaState.php +++ b/src/Illuminate/Database/Schema/SqliteSchemaState.php @@ -2,20 +2,22 @@ namespace Illuminate\Database\Schema; +use Illuminate\Database\Connection; + class SqliteSchemaState extends SchemaState { /** * Dump the database's schema into a file. * - * @param string $path - * + * @param \Illuminate\Database\Connection + * @param string $path * @return void */ - public function dump($path) + public function dump(Connection $connection, $path) { with($process = $this->makeProcess( $this->baseCommand().' .schema' - ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + ))->setTimeout(null)->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ // ])); @@ -32,12 +34,13 @@ public function dump($path) /** * Append the migration data to the schema dump. * + * @param string $path * @return void */ protected function appendMigrationData(string $path) { with($process = $this->makeProcess( - $this->baseCommand().' ".dump \'migrations\'"' + $this->baseCommand().' ".dump \''.$this->migrationTable.'\'"' ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ // ])); @@ -53,13 +56,12 @@ protected function appendMigrationData(string $path) /** * Load the given schema file into the database. * - * @param string $path - * + * @param string $path * @return void */ public function load($path) { - $process = $this->makeProcess($this->baseCommand().' < $LARAVEL_LOAD_PATH'); + $process = $this->makeProcess($this->baseCommand().' < "${:LARAVEL_LOAD_PATH}"'); $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, @@ -73,12 +75,13 @@ public function load($path) */ protected function baseCommand() { - return 'sqlite3 $LARAVEL_LOAD_DATABASE'; + return 'sqlite3 "${:LARAVEL_LOAD_DATABASE}"'; } /** * Get the base variables for a dump / load command. * + * @param array $config * @return array */ protected function baseVariables(array $config) diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index fdfca9114de1..b0b8490d062a 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -4,6 +4,8 @@ use Closure; use Doctrine\DBAL\Driver\PDOSqlsrv\Driver as DoctrineDriver; +use Doctrine\DBAL\Version; +use Illuminate\Database\PDO\SqlServerDriver; use Illuminate\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; use Illuminate\Database\Query\Processors\SqlServerProcessor; use Illuminate\Database\Schema\Grammars\SqlServerGrammar as SchemaGrammar; @@ -114,10 +116,10 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOSqlsrv\Driver + * @return \Doctrine\DBAL\Driver\PDOSqlsrv\Driver|\Illuminate\Database\PDO\SqlServerDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return class_exists(Version::class) ? new DoctrineDriver : new SqlServerDriver; } } diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index a807f59a3fc0..0a7cda072a90 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -15,14 +15,14 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "illuminate/collections": "^8.0", "illuminate/container": "^8.0", "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0", - "symfony/console": "^5.1" + "symfony/console": "^5.1.4" }, "autoload": { "psr-4": { @@ -35,13 +35,13 @@ } }, "suggest": { - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).", - "fzaninotto/faker": "Required to use the eloquent factory builder (^1.9.1).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "illuminate/console": "Required to use the database commands (^8.0).", "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/Encryption/composer.json b/src/Illuminate/Encryption/composer.json index daeae948b240..f90637f00a70 100644 --- a/src/Illuminate/Encryption/composer.json +++ b/src/Illuminate/Encryption/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", diff --git a/src/Illuminate/Events/CallQueuedListener.php b/src/Illuminate/Events/CallQueuedListener.php index c491f7c86c72..90ad3d9ac459 100644 --- a/src/Illuminate/Events/CallQueuedListener.php +++ b/src/Illuminate/Events/CallQueuedListener.php @@ -90,17 +90,15 @@ public function handle(Container $container) $this->job, $container->make($this->class) ); - call_user_func_array( - [$handler, $this->method], $this->data - ); + $handler->{$this->method}(...array_values($this->data)); } /** * Set the job instance of the given class if necessary. * * @param \Illuminate\Contracts\Queue\Job $job - * @param mixed $instance - * @return mixed + * @param object $instance + * @return object */ protected function setJobInstanceIfNecessary(Job $job, $instance) { @@ -125,10 +123,10 @@ public function failed($e) $handler = Container::getInstance()->make($this->class); - $parameters = array_merge($this->data, [$e]); + $parameters = array_merge(array_values($this->data), [$e]); if (method_exists($handler, 'failed')) { - call_user_func_array([$handler, 'failed'], $parameters); + $handler->failed(...$parameters); } } diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index a88a53822731..dac103348a7d 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -406,9 +406,9 @@ public function createClassListener($listener, $wildcard = false) return call_user_func($this->createClassCallable($listener), $event, $payload); } - return call_user_func_array( - $this->createClassCallable($listener), $payload - ); + $callable = $this->createClassCallable($listener); + + return $callable(...array_values($payload)); }; } @@ -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/Events/NullDispatcher.php b/src/Illuminate/Events/NullDispatcher.php index fe95477b90c2..dcfdc95f9e94 100644 --- a/src/Illuminate/Events/NullDispatcher.php +++ b/src/Illuminate/Events/NullDispatcher.php @@ -12,7 +12,7 @@ class NullDispatcher implements DispatcherContract /** * The underlying event dispatcher instance. * - * @var \Illuminate\Contracts\Bus\Dispatcher + * @var \Illuminate\Contracts\Events\Dispatcher */ protected $dispatcher; diff --git a/src/Illuminate/Events/composer.json b/src/Illuminate/Events/composer.json index 0db639289cb8..b77ba2c89685 100755 --- a/src/Illuminate/Events/composer.json +++ b/src/Illuminate/Events/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/bus": "^8.0", "illuminate/collections": "^8.0", "illuminate/container": "^8.0", diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 45be2cda09ee..1c33b9892676 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -52,9 +52,10 @@ public function __construct(FilesystemInterface $driver) * Assert that the given file exists. * * @param string|array $path + * @param string|null $content * @return $this */ - public function assertExists($path) + public function assertExists($path, $content = null) { $paths = Arr::wrap($path); @@ -62,6 +63,16 @@ public function assertExists($path) PHPUnit::assertTrue( $this->exists($path), "Unable to find a file at path [{$path}]." ); + + if (! is_null($content)) { + $actual = $this->get($path); + + PHPUnit::assertSame( + $content, + $actual, + "File [{$path}] was found, but content [{$actual}] does not match [{$content}]." + ); + } } return $this; @@ -745,6 +756,6 @@ protected function parseVisibility($visibility) */ public function __call($method, array $parameters) { - return call_user_func_array([$this->driver, $method], $parameters); + return $this->driver->{$method}(...array_values($parameters)); } } 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/LockableFile.php b/src/Illuminate/Filesystem/LockableFile.php new file mode 100644 index 000000000000..edb801f4847b --- /dev/null +++ b/src/Illuminate/Filesystem/LockableFile.php @@ -0,0 +1,183 @@ +path = $path; + + $this->ensureDirectoryExists($path); + $this->createResource($path, $mode); + } + + /** + * Create the file's directory if necessary. + * + * @param string $path + * @return void + */ + protected function ensureDirectoryExists($path) + { + if (! file_exists(dirname($path))) { + @mkdir(dirname($path), 0777, true); + } + } + + /** + * Create the file resource. + * + * @param string $path + * @param string $mode + * @return void + */ + protected function createResource($path, $mode) + { + $this->handle = @fopen($path, $mode); + } + + /** + * Read the file contents. + * + * @param int|null $length + * @return string + */ + public function read($length = null) + { + clearstatcache(true, $this->path); + + return fread($this->handle, $length ?? ($this->size() ?: 1)); + } + + /** + * Get the file size. + * + * @return int + */ + public function size() + { + return filesize($this->path); + } + + /** + * Write to the file. + * + * @param string $contents + * @return string + */ + public function write($contents) + { + fwrite($this->handle, $contents); + + fflush($this->handle); + + return $this; + } + + /** + * Truncate the file. + * + * @return $this + */ + public function truncate() + { + rewind($this->handle); + + ftruncate($this->handle, 0); + + return $this; + } + + /** + * Get a shared lock on the file. + * + * @param bool $block + * @return $this + */ + public function getSharedLock($block = false) + { + if (! flock($this->handle, LOCK_SH | ($block ? 0 : LOCK_NB))) { + throw new LockTimeoutException("Unable to acquire file lock at path [{$this->path}]."); + } + + $this->isLocked = true; + + return $this; + } + + /** + * Get an exclusive lock on the file. + * + * @param bool $block + * @return bool + */ + public function getExclusiveLock($block = false) + { + if (! flock($this->handle, LOCK_EX | ($block ? 0 : LOCK_NB))) { + throw new LockTimeoutException("Unable to acquire file lock at path [{$this->path}]."); + } + + $this->isLocked = true; + + return $this; + } + + /** + * Release the lock on the file. + * + * @return $this + */ + public function releaseLock() + { + flock($this->handle, LOCK_UN); + + $this->isLocked = false; + + return $this; + } + + /** + * Close the file. + * + * @return bool + */ + public function close() + { + if ($this->isLocked) { + $this->releaseLock(); + } + + return fclose($this->handle); + } +} diff --git a/src/Illuminate/Filesystem/composer.json b/src/Illuminate/Filesystem/composer.json index b45d18461fa5..16cb3b6d2edc 100644 --- a/src/Illuminate/Filesystem/composer.json +++ b/src/Illuminate/Filesystem/composer.json @@ -14,12 +14,12 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0", - "symfony/finder": "^5.1" + "symfony/finder": "^5.1.4" }, "autoload": { "psr-4": { @@ -34,13 +34,13 @@ "suggest": { "ext-ftp": "Required to use the Flysystem FTP driver.", "illuminate/http": "Required for handling uploaded files (^7.0).", - "league/flysystem": "Required to use the Flysystem local and FTP drivers (^1.0.34).", + "league/flysystem": "Required to use the Flysystem local and FTP drivers (^1.1).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", "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 13b858fdfe06..ec1deae44b68 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.6.0'; + const VERSION = '8.22.1'; /** * The base path for the Laravel installation. @@ -150,7 +150,7 @@ class Application extends Container implements ApplicationContract, CachesConfig /** * The prefixes of absolute cache paths for use during normalization. * - * @var array + * @var string[] */ protected $absoluteCachePathPrefixes = ['/', '\\']; @@ -1181,6 +1181,16 @@ public function getLocale() return $this['config']->get('app.locale'); } + /** + * Get the current application locale. + * + * @return string + */ + public function currentLocale() + { + return $this->getLocale(); + } + /** * Get the current application fallback locale. * @@ -1301,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/Auth/Access/Authorizable.php b/src/Illuminate/Foundation/Auth/Access/Authorizable.php index dd0ba609fbab..d8cf50dbc537 100644 --- a/src/Illuminate/Foundation/Auth/Access/Authorizable.php +++ b/src/Illuminate/Foundation/Auth/Access/Authorizable.php @@ -18,6 +18,18 @@ public function can($abilities, $arguments = []) return app(Gate::class)->forUser($this)->check($abilities, $arguments); } + /** + * Determine if the entity has any of the given abilities. + * + * @param iterable|string $abilities + * @param array|mixed $arguments + * @return bool + */ + public function canAny($abilities, $arguments = []) + { + return app(Gate::class)->forUser($this)->any($abilities, $arguments); + } + /** * Determine if the entity does not have the given abilities. * diff --git a/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php b/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php index 7f36ca17bae9..c9c43046ed2c 100644 --- a/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php +++ b/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php @@ -46,9 +46,11 @@ public function rules() */ public function fulfill() { - $this->user()->markEmailAsVerified(); + if (! $this->user()->hasVerifiedEmail()) { + $this->user()->markEmailAsVerified(); - event(new Verified($this->user())); + event(new Verified($this->user())); + } } /** 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/PendingChain.php b/src/Illuminate/Foundation/Bus/PendingChain.php index 8965e9923fa1..5931fb3202b2 100644 --- a/src/Illuminate/Foundation/Bus/PendingChain.php +++ b/src/Illuminate/Foundation/Bus/PendingChain.php @@ -37,6 +37,13 @@ class PendingChain */ public $queue; + /** + * The number of seconds before the chain should be made available. + * + * @var \DateTimeInterface|\DateInterval|int|null + */ + public $delay; + /** * The callbacks to be executed on failure. * @@ -83,6 +90,19 @@ public function onQueue($queue) return $this; } + /** + * Set the desired delay for the chain. + * + * @param \DateTimeInterface|\DateInterval|int|null $delay + * @return $this + */ + public function delay($delay) + { + $this->delay = $delay; + + return $this; + } + /** * Add a callback to be executed on job failure. * @@ -123,8 +143,20 @@ public function dispatch() $firstJob = $this->job; } - $firstJob->allOnConnection($this->connection); - $firstJob->allOnQueue($this->queue); + if ($this->connection) { + $firstJob->chainConnection = $this->connection; + $firstJob->connection = $firstJob->connection ?: $this->connection; + } + + if ($this->queue) { + $firstJob->chainQueue = $this->queue; + $firstJob->queue = $firstJob->queue ?: $this->queue; + } + + if ($this->delay) { + $firstJob->delay = ! is_null($firstJob->delay) ? $firstJob->delay : $this->delay; + } + $firstJob->chain($this->chain); $firstJob->chainCatchCallbacks = $this->catchCallbacks(); diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index 89329515ecd2..8ac4432319a9 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -2,7 +2,10 @@ namespace Illuminate\Foundation\Bus; +use Illuminate\Container\Container; use Illuminate\Contracts\Bus\Dispatcher; +use Illuminate\Contracts\Cache\Repository as Cache; +use Illuminate\Contracts\Queue\ShouldBeUnique; class PendingDispatch { @@ -96,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. * @@ -121,6 +148,45 @@ public function afterResponse() return $this; } + /** + * Determine if the job should be dispatched. + * + * @return bool + */ + protected function shouldDispatch() + { + if (! $this->job instanceof ShouldBeUnique) { + return true; + } + + $uniqueId = method_exists($this->job, 'uniqueId') + ? $this->job->uniqueId() + : ($this->job->uniqueId ?? ''); + + $cache = method_exists($this->job, 'uniqueVia') + ? $this->job->uniqueVia() + : Container::getInstance()->make(Cache::class); + + return (bool) $cache->lock( + $key = 'laravel_unique_job:'.get_class($this->job).$uniqueId, + $this->job->uniqueFor ?? 0 + )->get(); + } + + /** + * Dynamically proxy methods to the underlying job. + * + * @param string $method + * @param array $parameters + * @return $this + */ + public function __call($method, $parameters) + { + $this->job->{$method}(...$parameters); + + return $this; + } + /** * Handle the object's destruction. * @@ -128,7 +194,9 @@ public function afterResponse() */ public function __destruct() { - if ($this->afterResponse) { + if (! $this->shouldDispatch()) { + return; + } elseif ($this->afterResponse) { app(Dispatcher::class)->dispatchAfterResponse($this->job); } else { app(Dispatcher::class)->dispatch($this->job); diff --git a/src/Illuminate/Foundation/Console/CastMakeCommand.php b/src/Illuminate/Foundation/Console/CastMakeCommand.php index fd390de10406..3fa3a667fff3 100644 --- a/src/Illuminate/Foundation/Console/CastMakeCommand.php +++ b/src/Illuminate/Foundation/Console/CastMakeCommand.php @@ -34,7 +34,20 @@ class CastMakeCommand extends GeneratorCommand */ protected function getStub() { - return __DIR__.'/stubs/cast.stub'; + return $this->resolveStubPath('/stubs/cast.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; } /** 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 e6b0798490e7..6a8eeb0c86f9 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -57,7 +57,7 @@ class Kernel implements KernelContract /** * The bootstrap classes for the application. * - * @var array + * @var string[] */ protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, @@ -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')); } /** @@ -223,7 +223,7 @@ protected function load($paths) $command = $namespace.str_replace( ['/', '.php'], ['\\', ''], - Str::after($command->getPathname(), realpath(app_path()).DIRECTORY_SEPARATOR) + Str::after($command->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR) ); if (is_subclass_of($command, Command::class) && diff --git a/src/Illuminate/Foundation/Console/ModelMakeCommand.php b/src/Illuminate/Foundation/Console/ModelMakeCommand.php index ceee516bb959..95209bba050e 100644 --- a/src/Illuminate/Foundation/Console/ModelMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ModelMakeCommand.php @@ -108,7 +108,7 @@ protected function createSeeder() { $seeder = Str::studly(class_basename($this->argument('name'))); - $this->call('make:seed', [ + $this->call('make:seeder', [ 'name' => "{$seeder}Seeder", ]); } diff --git a/src/Illuminate/Foundation/Console/ObserverMakeCommand.php b/src/Illuminate/Foundation/Console/ObserverMakeCommand.php index f34744f4a3ef..a2661f3fabde 100644 --- a/src/Illuminate/Foundation/Console/ObserverMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ObserverMakeCommand.php @@ -3,7 +3,7 @@ namespace Illuminate\Foundation\Console; use Illuminate\Console\GeneratorCommand; -use Illuminate\Support\Str; +use InvalidArgumentException; use Symfony\Component\Console\Input\InputOption; class ObserverMakeCommand extends GeneratorCommand @@ -45,47 +45,73 @@ protected function buildClass($name) } /** - * Get the stub file for the generator. + * Replace the model for the given stub. * + * @param string $stub + * @param string $model * @return string */ - protected function getStub() + protected function replaceModel($stub, $model) { - return $this->option('model') - ? __DIR__.'/stubs/observer.stub' - : __DIR__.'/stubs/observer.plain.stub'; + $modelClass = $this->parseModel($model); + + $replace = [ + 'DummyFullModelClass' => $modelClass, + '{{ namespacedModel }}' => $modelClass, + '{{namespacedModel}}' => $modelClass, + 'DummyModelClass' => class_basename($modelClass), + '{{ model }}' => class_basename($modelClass), + '{{model}}' => class_basename($modelClass), + 'DummyModelVariable' => lcfirst(class_basename($modelClass)), + '{{ modelVariable }}' => lcfirst(class_basename($modelClass)), + '{{modelVariable}}' => lcfirst(class_basename($modelClass)), + ]; + + return str_replace( + array_keys($replace), array_values($replace), $stub + ); } /** - * Replace the model for the given stub. + * Get the fully-qualified model class name. * - * @param string $stub * @param string $model * @return string + * + * @throws \InvalidArgumentException */ - protected function replaceModel($stub, $model) + protected function parseModel($model) { - $model = str_replace('/', '\\', $model); - - $namespacedModel = $this->qualifyModel($model); - - if (Str::startsWith($model, '\\')) { - $stub = str_replace('NamespacedDummyModel', trim($model, '\\'), $stub); - } else { - $stub = str_replace('NamespacedDummyModel', $namespacedModel, $stub); + if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) { + throw new InvalidArgumentException('Model name contains invalid characters.'); } - $stub = str_replace( - "use {$namespacedModel};\nuse {$namespacedModel};", "use {$namespacedModel};", $stub - ); - - $model = class_basename(trim($model, '\\')); - - $stub = str_replace('DocDummyModel', Str::snake($model, ' '), $stub); + return $this->qualifyModel($model); + } - $stub = str_replace('DummyModel', $model, $stub); + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return $this->option('model') + ? $this->resolveStubPath('/stubs/observer.stub') + : $this->resolveStubPath('/stubs/observer.plain.stub'); + } - return str_replace('dummyModel', Str::camel($model), $stub); + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; } /** diff --git a/src/Illuminate/Foundation/Console/OptimizeCommand.php b/src/Illuminate/Foundation/Console/OptimizeCommand.php index f42b75552016..af5d731ea8da 100644 --- a/src/Illuminate/Foundation/Console/OptimizeCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeCommand.php @@ -29,7 +29,6 @@ public function handle() { $this->call('config:cache'); $this->call('route:cache'); - $this->call('view:cache'); $this->info('Files cached successfully!'); } diff --git a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php index 79dde7d396f2..0fe6964d5940 100644 --- a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php +++ b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\GeneratorCommand; use Illuminate\Support\Str; +use LogicException; use Symfony\Component\Console\Input\InputOption; class PolicyMakeCommand extends GeneratorCommand @@ -78,8 +79,12 @@ protected function userProviderModel() $guard = $this->option('guard') ?: $config->get('auth.defaults.guard'); + if (is_null($guardProvider = $config->get('auth.guards.'.$guard.'.provider'))) { + throw new LogicException('The ['.$guard.'] guard is not defined in your "auth" configuration file.'); + } + return $config->get( - 'auth.providers.'.$config->get('auth.guards.'.$guard.'.provider').'.model' + 'auth.providers.'.$guardProvider.'.model' ); } diff --git a/src/Illuminate/Foundation/Console/QueuedCommand.php b/src/Illuminate/Foundation/Console/QueuedCommand.php index 67749ee938dd..fb3d027b4b0a 100644 --- a/src/Illuminate/Foundation/Console/QueuedCommand.php +++ b/src/Illuminate/Foundation/Console/QueuedCommand.php @@ -37,6 +37,6 @@ public function __construct($data) */ public function handle(KernelContract $kernel) { - call_user_func_array([$kernel, 'call'], $this->data); + $kernel->call(...array_values($this->data)); } } diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index dca0156ab14b..d14e58cf6315 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -36,14 +36,14 @@ class RouteListCommand extends Command /** * The table headers for the command. * - * @var array + * @var string[] */ protected $headers = ['Domain', 'Method', 'URI', 'Name', 'Action', 'Middleware']; /** * The columns to display when using the "compact" flag. * - * @var array + * @var string[] */ protected $compactColumns = ['method', 'uri', 'action']; diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index ab4cb2ef2874..20b7f59c8f7f 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -61,7 +61,9 @@ public function handle() clearstatcache(false, $environmentFile); } - if ($hasEnvironment && filemtime($environmentFile) > $environmentLastModified) { + if (! $this->option('no-reload') && + $hasEnvironment && + filemtime($environmentFile) > $environmentLastModified) { $environmentLastModified = filemtime($environmentFile); $this->comment('Environment modified. Restarting server...'); @@ -93,7 +95,11 @@ public function handle() protected function startProcess() { $process = new Process($this->serverCommand(), null, collect($_ENV)->mapWithKeys(function ($value, $key) { - return $key === 'APP_ENV' + if ($this->option('no-reload')) { + return [$key => $value]; + } + + return in_array($key, ['APP_ENV', 'LARAVEL_SAIL']) ? [$key => $value] : [$key => false]; })->all()); @@ -164,6 +170,7 @@ protected function getOptions() ['host', null, InputOption::VALUE_OPTIONAL, 'The host address to serve the application on', '127.0.0.1'], ['port', null, InputOption::VALUE_OPTIONAL, 'The port to serve the application on', Env::get('SERVER_PORT')], ['tries', null, InputOption::VALUE_OPTIONAL, 'The max number of ports to attempt to serve from', 10], + ['no-reload', null, InputOption::VALUE_NONE, 'Do not reload the development server on .env file changes'], ]; } } diff --git a/src/Illuminate/Foundation/Console/StubPublishCommand.php b/src/Illuminate/Foundation/Console/StubPublishCommand.php index 55e86a958423..4f3f087d4b17 100644 --- a/src/Illuminate/Foundation/Console/StubPublishCommand.php +++ b/src/Illuminate/Foundation/Console/StubPublishCommand.php @@ -33,10 +33,13 @@ public function handle() } $files = [ + __DIR__.'/stubs/cast.stub' => $stubsPath.'/cast.stub', __DIR__.'/stubs/job.queued.stub' => $stubsPath.'/job.queued.stub', __DIR__.'/stubs/job.stub' => $stubsPath.'/job.stub', __DIR__.'/stubs/model.pivot.stub' => $stubsPath.'/model.pivot.stub', __DIR__.'/stubs/model.stub' => $stubsPath.'/model.stub', + __DIR__.'/stubs/observer.stub' => $stubsPath.'/observer.stub', + __DIR__.'/stubs/observer.plain.stub' => $stubsPath.'/observer.plain.stub', __DIR__.'/stubs/request.stub' => $stubsPath.'/request.stub', __DIR__.'/stubs/resource.stub' => $stubsPath.'/resource.stub', __DIR__.'/stubs/resource-collection.stub' => $stubsPath.'/resource-collection.stub', diff --git a/src/Illuminate/Foundation/Console/TestMakeCommand.php b/src/Illuminate/Foundation/Console/TestMakeCommand.php index 0a176cbaba7b..10814a0b09d2 100644 --- a/src/Illuminate/Foundation/Console/TestMakeCommand.php +++ b/src/Illuminate/Foundation/Console/TestMakeCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\GeneratorCommand; use Illuminate\Support\Str; +use Symfony\Component\Console\Input\InputOption; class TestMakeCommand extends GeneratorCommand { @@ -12,7 +13,7 @@ class TestMakeCommand extends GeneratorCommand * * @var string */ - protected $signature = 'make:test {name : The name of the class} {--unit : Create a unit test}'; + protected $name = 'make:test'; /** * The console command description. @@ -90,4 +91,16 @@ protected function rootNamespace() { return 'Tests'; } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['unit', 'u', InputOption::VALUE_NONE, 'Create a unit test.'], + ]; + } } diff --git a/src/Illuminate/Foundation/Console/stubs/cast.stub b/src/Illuminate/Foundation/Console/stubs/cast.stub index 26bfd985e63c..a496e5acc022 100644 --- a/src/Illuminate/Foundation/Console/stubs/cast.stub +++ b/src/Illuminate/Foundation/Console/stubs/cast.stub @@ -1,10 +1,10 @@ get('/'); diff --git a/src/Illuminate/Foundation/Console/stubs/test.unit.stub b/src/Illuminate/Foundation/Console/stubs/test.unit.stub index 98af6529355c..b6816aa72f29 100644 --- a/src/Illuminate/Foundation/Console/stubs/test.unit.stub +++ b/src/Illuminate/Foundation/Console/stubs/test.unit.stub @@ -11,7 +11,7 @@ class {{ class }} extends TestCase * * @return void */ - public function testExample() + public function test_example() { $this->assertTrue(true); } diff --git a/src/Illuminate/Foundation/Console/stubs/view-component.stub b/src/Illuminate/Foundation/Console/stubs/view-component.stub index 20cdaa2a8ef9..5c6ecc586ec4 100644 --- a/src/Illuminate/Foundation/Console/stubs/view-component.stub +++ b/src/Illuminate/Foundation/Console/stubs/view-component.stub @@ -19,7 +19,7 @@ class DummyClass extends Component /** * Get the view / contents that represent the component. * - * @return \Illuminate\View\View|string + * @return \Illuminate\Contracts\View\View|string */ public function render() { 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 7fcd81565e61..924046b8b340 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -20,6 +20,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; +use Illuminate\Support\Reflector; use Illuminate\Support\Traits\ReflectsClosures; use Illuminate\Support\ViewErrorBag; use Illuminate\Validation\ValidationException; @@ -80,7 +81,7 @@ class Handler implements ExceptionHandlerContract /** * A list of the internal exception types that should not be reported. * - * @var array + * @var string[] */ protected $internalDontReport = [ AuthenticationException::class, @@ -96,7 +97,7 @@ class Handler implements ExceptionHandlerContract /** * A list of the inputs that are never flashed for validation exceptions. * - * @var array + * @var string[] */ protected $dontFlash = [ 'password', @@ -209,7 +210,7 @@ public function report(Throwable $e) return; } - if (is_callable($reportCallable = [$e, 'report'])) { + if (Reflector::isCallable($reportCallable = [$e, 'report'])) { if ($this->container->call($reportCallable) !== false) { return; } @@ -464,7 +465,7 @@ protected function prepareResponse($request, Throwable $e) */ protected function convertExceptionToResponse(Throwable $e) { - return SymfonyResponse::create( + return new SymfonyResponse( $this->renderExceptionContent($e), $this->isHttpException($e) ? $e->getStatusCode() : 500, $this->isHttpException($e) ? $e->getHeaders() : [] diff --git a/src/Illuminate/Foundation/Exceptions/ReportableHandler.php b/src/Illuminate/Foundation/Exceptions/ReportableHandler.php index 154d9260d7ef..3664bc6bea25 100644 --- a/src/Illuminate/Foundation/Exceptions/ReportableHandler.php +++ b/src/Illuminate/Foundation/Exceptions/ReportableHandler.php @@ -42,7 +42,11 @@ public function __construct(callable $callback) */ public function __invoke(Throwable $e) { - call_user_func($this->callback, $e); + $result = call_user_func($this->callback, $e); + + if ($result === false) { + return false; + } return ! $this->shouldStop; } 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/Kernel.php b/src/Illuminate/Foundation/Http/Kernel.php index a1f3fb9aee50..656bf0f1d164 100644 --- a/src/Illuminate/Foundation/Http/Kernel.php +++ b/src/Illuminate/Foundation/Http/Kernel.php @@ -31,7 +31,7 @@ class Kernel implements KernelContract /** * The bootstrap classes for the application. * - * @var array + * @var string[] */ protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, @@ -68,7 +68,7 @@ class Kernel implements KernelContract * * Forces non-global middleware to always be in the given order. * - * @var array + * @var string[] */ protected $middlewarePriority = [ \Illuminate\Cookie\Middleware\EncryptCookies::class, 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 bc17f9c1bc79..4426e4b5d34c 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -7,8 +7,12 @@ 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; use Illuminate\Database\Console\DumpCommand; use Illuminate\Database\Console\Factories\FactoryMakeCommand; use Illuminate\Database\Console\Seeds\SeedCommand; @@ -63,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; @@ -87,6 +92,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'ClearResets' => 'command.auth.resets.clear', 'ConfigCache' => 'command.config.cache', 'ConfigClear' => 'command.config.clear', + 'Db' => DbCommand::class, 'DbWipe' => 'command.db.wipe', 'Down' => 'command.down', 'Environment' => 'command.environment', @@ -102,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', @@ -112,7 +119,10 @@ 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', 'ViewCache' => 'command.view.cache', @@ -180,7 +190,7 @@ public function register() protected function registerCommands(array $commands) { foreach (array_keys($commands) as $command) { - call_user_func_array([$this, "register{$command}Command"], []); + $this->{"register{$command}Command"}(); } $this->commands(array_values($commands)); @@ -330,6 +340,16 @@ protected function registerControllerMakeCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerDbCommand() + { + $this->app->singleton(DbCommand::class); + } + /** * Register the command. * @@ -666,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. * @@ -904,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. * @@ -914,6 +956,26 @@ protected function registerScheduleRunCommand() $this->app->singleton(ScheduleRunCommand::class); } + /** + * Register the command. + * + * @return void + */ + protected function registerScheduleTestCommand() + { + $this->app->singleton(ScheduleTestCommand::class); + } + + /** + * Register the command. + * + * @return void + */ + protected function registerScheduleWorkCommand() + { + $this->app->singleton(ScheduleWorkCommand::class); + } + /** * Register the command. * diff --git a/src/Illuminate/Foundation/Providers/ConsoleSupportServiceProvider.php b/src/Illuminate/Foundation/Providers/ConsoleSupportServiceProvider.php index b23f18731a21..f6131ca5e105 100644 --- a/src/Illuminate/Foundation/Providers/ConsoleSupportServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ConsoleSupportServiceProvider.php @@ -11,7 +11,7 @@ class ConsoleSupportServiceProvider extends AggregateServiceProvider implements /** * The provider class names. * - * @var array + * @var string[] */ protected $providers = [ ArtisanServiceProvider::class, diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 82836ca04423..2f39afd43d36 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -12,7 +12,7 @@ class FoundationServiceProvider extends AggregateServiceProvider /** * The provider class names. * - * @var array + * @var string[] */ protected $providers = [ FormRequestServiceProvider::class, diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php index b34777910ea7..dad0f65f6464 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php @@ -22,6 +22,13 @@ trait InteractsWithConsole */ public $expectedOutput = []; + /** + * All of the output lines that aren't expected to be displayed. + * + * @var array + */ + public $unexpectedOutput = []; + /** * All of the expected ouput tables. * 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/InteractsWithTime.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php index d6413a528062..184a2441ce86 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php @@ -44,8 +44,6 @@ public function travelTo(DateTimeInterface $date, $callback = null) */ public function travelBack() { - Carbon::setTestNow(); - - return Carbon::now(); + return Wormhole::back(); } } 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/Wormhole.php b/src/Illuminate/Foundation/Testing/Wormhole.php index 59512981e6ad..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,11 +110,23 @@ 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); } + /** + * Travel back to the current time. + * + * @return \DateTimeInterface + */ + public static function back() + { + Date::setTestNow(); + + return Date::now(); + } + /** * Handle the given optional execution callback. * @@ -125,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 55cb5bcc5ca6..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) { @@ -673,7 +677,7 @@ function resource_path($path = '') /** * Return a new response from the application. * - * @param \Illuminate\View\View|string|array|null $content + * @param \Illuminate\Contracts\View\View|string|array|null $content * @param int $status * @param array $headers * @return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory @@ -884,7 +888,7 @@ function validator(array $data = [], array $rules = [], array $messages = [], ar * @param string|null $view * @param \Illuminate\Contracts\Support\Arrayable|array $data * @param array $mergeData - * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory */ function view($view = null, $data = [], $mergeData = []) { diff --git a/src/Illuminate/Hashing/composer.json b/src/Illuminate/Hashing/composer.json index 370e4897ae79..6ad3411c7cfc 100755 --- a/src/Illuminate/Hashing/composer.json +++ b/src/Illuminate/Hashing/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/contracts": "^8.0", "illuminate/support": "^8.0" }, diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 3a5b9db95085..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. * @@ -298,6 +320,16 @@ public function recorded($callback = null) }); } + /** + * Create a new pending request instance for this factory. + * + * @return \Illuminate\Http\Client\PendingRequest + */ + protected function newPendingRequest() + { + return new PendingRequest($this); + } + /** * Execute a method against a new pending request instance. * @@ -311,7 +343,7 @@ public function __call($method, $parameters) return $this->macroCall($method, $parameters); } - return tap(new PendingRequest($this), function ($request) { + return tap($this->newPendingRequest(), function ($request) { $request->stub($this->stubCallbacks); })->{$method}(...$parameters); } diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 08587e1e1fd9..836e8b39fe50 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -87,7 +87,7 @@ class PendingRequest /** * The callbacks that should execute before the request is sent. * - * @var array + * @var \Illuminate\Support\Collection */ protected $beforeSendingCallbacks; @@ -181,14 +181,22 @@ public function asForm() /** * Attach a file to the request. * - * @param string $name + * @param string|array $name * @param string $contents * @param string|null $filename * @param array $headers * @return $this */ - public function attach($name, $contents, $filename = null, array $headers = []) + public function attach($name, $contents = '', $filename = null, array $headers = []) { + if (is_array($name)) { + foreach ($name as $file) { + $this->attach(...$file); + } + + return $this; + } + $this->asMultipart(); $this->pendingFiles[] = array_filter([ @@ -313,6 +321,17 @@ public function withToken($token, $type = 'Bearer') }); } + /** + * Specify the user agent for the request. + * + * @param string $userAgent + * @return $this + */ + public function withUserAgent($userAgent) + { + return $this->withHeaders(['User-Agent' => $userAgent]); + } + /** * Specify the cookies that should be included with the request. * diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index 2a34b5f83fef..f65a8d5ca1ba 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -180,6 +180,21 @@ public function serverError() return $this->status() >= 500; } + /** + * Execute the given callback if there was a server or client error. + * + * @param \Closure|callable $callback + * @return $this + */ + public function onError(callable $callback) + { + if ($this->failed()) { + $callback($this); + } + + return $this; + } + /** * Get the response cookies. * @@ -190,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. * @@ -203,14 +228,21 @@ public function toPsrResponse() /** * Throw an exception if a server or client error occurred. * + * @param \Closure|null $callback * @return $this * * @throws \Illuminate\Http\Client\RequestException */ public function throw() { - if ($this->serverError() || $this->clientError()) { - throw new RequestException($this); + $callback = func_get_args()[0] ?? null; + + if ($this->failed()) { + throw tap(new RequestException($this), function ($exception) use ($callback) { + if ($callback && is_callable($callback)) { + $callback($this, $exception); + } + }); } return $this; diff --git a/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php b/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php index be760a2619d9..f938bf48492f 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. * @@ -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/RedirectResponse.php b/src/Illuminate/Http/RedirectResponse.php index 7f256a399396..32bb5fcffb95 100755 --- a/src/Illuminate/Http/RedirectResponse.php +++ b/src/Illuminate/Http/RedirectResponse.php @@ -145,6 +145,21 @@ public function withErrors($provider, $key = 'default') return $this; } + /** + * Parse the given errors into an appropriate value. + * + * @param \Illuminate\Contracts\Support\MessageProvider|array|string $provider + * @return \Illuminate\Support\MessageBag + */ + protected function parseErrors($provider) + { + if ($provider instanceof MessageProvider) { + return $provider->getMessageBag(); + } + + return new MessageBag((array) $provider); + } + /** * Add a fragment identifier to the URL. * @@ -167,21 +182,6 @@ public function withoutFragment() return $this->setTargetUrl(Str::before($this->getTargetUrl(), '#')); } - /** - * Parse the given errors into an appropriate value. - * - * @param \Illuminate\Contracts\Support\MessageProvider|array|string $provider - * @return \Illuminate\Support\MessageBag - */ - protected function parseErrors($provider) - { - if ($provider instanceof MessageProvider) { - return $provider->getMessageBag(); - } - - return new MessageBag((array) $provider); - } - /** * Get the original response content. * diff --git a/src/Illuminate/Http/Resources/CollectsResources.php b/src/Illuminate/Http/Resources/CollectsResources.php index a5531f7a02ce..5c42da4225f5 100644 --- a/src/Illuminate/Http/Resources/CollectsResources.php +++ b/src/Illuminate/Http/Resources/CollectsResources.php @@ -47,7 +47,8 @@ protected function collects() } if (Str::endsWith(class_basename($this), 'Collection') && - class_exists($class = Str::replaceLast('Collection', '', get_class($this)))) { + (class_exists($class = Str::replaceLast('Collection', '', get_class($this))) || + class_exists($class = Str::replaceLast('Collection', 'Resource', get_class($this))))) { return $class; } } diff --git a/src/Illuminate/Http/Resources/Json/ResourceCollection.php b/src/Illuminate/Http/Resources/Json/ResourceCollection.php index f71fd0b3fc02..2931fd6463c7 100644 --- a/src/Illuminate/Http/Resources/Json/ResourceCollection.php +++ b/src/Illuminate/Http/Resources/Json/ResourceCollection.php @@ -35,7 +35,7 @@ class ResourceCollection extends JsonResource implements Countable, IteratorAggr /** * The query parameters that should be added to the pagination links. * - * @var array + * @var array|null */ protected $queryParameters; diff --git a/src/Illuminate/Http/ResponseTrait.php b/src/Illuminate/Http/ResponseTrait.php index b52ddcdbde35..a255bcf9376b 100644 --- a/src/Illuminate/Http/ResponseTrait.php +++ b/src/Illuminate/Http/ResponseTrait.php @@ -96,7 +96,7 @@ public function withHeaders($headers) */ public function cookie($cookie) { - return call_user_func_array([$this, 'withCookie'], func_get_args()); + return $this->withCookie(...func_get_args()); } /** @@ -108,7 +108,26 @@ public function cookie($cookie) public function withCookie($cookie) { if (is_string($cookie) && function_exists('cookie')) { - $cookie = call_user_func_array('cookie', func_get_args()); + $cookie = cookie(...func_get_args()); + } + + $this->headers->setCookie($cookie); + + return $this; + } + + /** + * Expire a cookie when sending the response. + * + * @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie + * @param string|null $path + * @param string|null $domain + * @return $this + */ + public function withoutCookie($cookie, $path = null, $domain = null) + { + if (is_string($cookie) && function_exists('cookie')) { + $cookie = cookie($cookie, null, -2628000, $path, $domain); } $this->headers->setCookie($cookie); diff --git a/src/Illuminate/Http/UploadedFile.php b/src/Illuminate/Http/UploadedFile.php index 4e9f6f65bf5f..7779683e4d0a 100644 --- a/src/Illuminate/Http/UploadedFile.php +++ b/src/Illuminate/Http/UploadedFile.php @@ -91,7 +91,7 @@ public function storeAs($path, $name, $options = []) /** * Get the contents of the uploaded file. * - * @return bool|string + * @return false|string * * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException */ diff --git a/src/Illuminate/Http/composer.json b/src/Illuminate/Http/composer.json index 9a510f0e8071..8bf355434520 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -14,15 +14,15 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "illuminate/collections": "^8.0", "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/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index ab9bf51a15a4..0e0abcd67c33 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -237,6 +237,10 @@ protected function createCustomDriver(array $config) */ protected function createStackDriver(array $config) { + if (is_string($config['channels'])) { + $config['channels'] = explode(',', $config['channels']); + } + $handlers = collect($config['channels'])->flatMap(function ($channel) { return $this->channel($channel)->getHandlers(); })->all(); diff --git a/src/Illuminate/Log/composer.json b/src/Illuminate/Log/composer.json index 822cb950275f..1fd148d9a9fd 100755 --- a/src/Illuminate/Log/composer.json +++ b/src/Illuminate/Log/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/contracts": "^8.0", "illuminate/support": "^8.0", "monolog/monolog": "^2.0" diff --git a/src/Illuminate/Macroable/Traits/Macroable.php b/src/Illuminate/Macroable/Traits/Macroable.php index 0c2112c7fba5..406f65edc79b 100644 --- a/src/Illuminate/Macroable/Traits/Macroable.php +++ b/src/Illuminate/Macroable/Traits/Macroable.php @@ -82,7 +82,7 @@ public static function __callStatic($method, $parameters) $macro = static::$macros[$method]; if ($macro instanceof Closure) { - return call_user_func_array(Closure::bind($macro, null, static::class), $parameters); + $macro = $macro->bindTo(null, static::class); } return $macro(...$parameters); @@ -108,7 +108,7 @@ public function __call($method, $parameters) $macro = static::$macros[$method]; if ($macro instanceof Closure) { - return call_user_func_array($macro->bindTo($this, static::class), $parameters); + $macro = $macro->bindTo($this, static::class); } return $macro(...$parameters); diff --git a/src/Illuminate/Macroable/composer.json b/src/Illuminate/Macroable/composer.json index ba9bb77d604c..dfa5c62be192 100644 --- a/src/Illuminate/Macroable/composer.json +++ b/src/Illuminate/Macroable/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3" + "php": "^7.3|^8.0" }, "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..84e14b4a7b98 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -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/Markdown.php b/src/Illuminate/Mail/Markdown.php index 3effcf92bf57..a506f837f59f 100644 --- a/src/Illuminate/Mail/Markdown.php +++ b/src/Illuminate/Mail/Markdown.php @@ -174,4 +174,14 @@ public function theme($theme) return $this; } + + /** + * Get the theme currently being used by the renderer. + * + * @return string + */ + public function getTheme() + { + return $this->theme; + } } 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/Mail/Transport/MailgunTransport.php b/src/Illuminate/Mail/Transport/MailgunTransport.php index 195c00032464..1c862b1a7f30 100644 --- a/src/Illuminate/Mail/Transport/MailgunTransport.php +++ b/src/Illuminate/Mail/Transport/MailgunTransport.php @@ -72,9 +72,10 @@ public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = nul $this->payload($message, $to) ); - $message->getHeaders()->addTextHeader( - 'X-Mailgun-Message-ID', $this->getMessageId($response) - ); + $messageId = $this->getMessageId($response); + + $message->getHeaders()->addTextHeader('X-Message-ID', $messageId); + $message->getHeaders()->addTextHeader('X-Mailgun-Message-ID', $messageId); $message->setBcc($bcc); diff --git a/src/Illuminate/Mail/Transport/SesTransport.php b/src/Illuminate/Mail/Transport/SesTransport.php index 0dc8584a4edc..76eb2a8a03c3 100644 --- a/src/Illuminate/Mail/Transport/SesTransport.php +++ b/src/Illuminate/Mail/Transport/SesTransport.php @@ -52,7 +52,10 @@ public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = nul ) ); - $message->getHeaders()->addTextHeader('X-SES-Message-ID', $result->get('MessageId')); + $messageId = $result->get('MessageId'); + + $message->getHeaders()->addTextHeader('X-Message-ID', $messageId); + $message->getHeaders()->addTextHeader('X-SES-Message-ID', $messageId); $this->sendPerformed($message); diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index 9842c22f6fee..433271509886 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "illuminate/collections": "^8.0", "illuminate/container": "^8.0", @@ -37,7 +37,7 @@ } }, "suggest": { - "aws/aws-sdk-php": "Required to use the SES mail driver (^3.0).", + "aws/aws-sdk-php": "Required to use the SES mail driver (^3.155).", "guzzlehttp/guzzle": "Required to use the Mailgun mail driver (^6.5.5|^7.0.1).", "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)." }, diff --git a/src/Illuminate/Notifications/DatabaseNotification.php b/src/Illuminate/Notifications/DatabaseNotification.php index 0dfc7e53015c..14bc9d659f97 100644 --- a/src/Illuminate/Notifications/DatabaseNotification.php +++ b/src/Illuminate/Notifications/DatabaseNotification.php @@ -2,6 +2,7 @@ namespace Illuminate\Notifications; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class DatabaseNotification extends Model @@ -98,6 +99,28 @@ public function unread() return $this->read_at === null; } + /** + * Scope a query to only include read notifications. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeRead(Builder $query) + { + return $query->whereNotNull('read_at'); + } + + /** + * Scope a query to only include unread notifications. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeUnread(Builder $query) + { + return $query->whereNull('read_at'); + } + /** * Create a new database notification collection instance. * diff --git a/src/Illuminate/Notifications/Events/BroadcastNotificationCreated.php b/src/Illuminate/Notifications/Events/BroadcastNotificationCreated.php index 77498ea39874..24958852758b 100644 --- a/src/Illuminate/Notifications/Events/BroadcastNotificationCreated.php +++ b/src/Illuminate/Notifications/Events/BroadcastNotificationCreated.php @@ -92,6 +92,10 @@ protected function channelName() */ public function broadcastWith() { + if (method_exists($this->notification, 'broadcastWith')) { + return $this->notification->broadcastWith(); + } + return array_merge($this->data, [ 'id' => $this->notification->id, 'type' => $this->broadcastType(), diff --git a/src/Illuminate/Notifications/HasDatabaseNotifications.php b/src/Illuminate/Notifications/HasDatabaseNotifications.php index 981d8e552583..5f999da9a34d 100644 --- a/src/Illuminate/Notifications/HasDatabaseNotifications.php +++ b/src/Illuminate/Notifications/HasDatabaseNotifications.php @@ -21,7 +21,7 @@ public function notifications() */ public function readNotifications() { - return $this->notifications()->whereNotNull('read_at'); + return $this->notifications()->read(); } /** @@ -31,6 +31,6 @@ public function readNotifications() */ public function unreadNotifications() { - return $this->notifications()->whereNull('read_at'); + return $this->notifications()->unread(); } } diff --git a/src/Illuminate/Notifications/Messages/MailMessage.php b/src/Illuminate/Notifications/Messages/MailMessage.php index 08ee2f1f7433..08e79d0fa0f5 100644 --- a/src/Illuminate/Notifications/Messages/MailMessage.php +++ b/src/Illuminate/Notifications/Messages/MailMessage.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Renderable; use Illuminate\Mail\Markdown; -use Traversable; class MailMessage extends SimpleMessage implements Renderable { @@ -297,9 +296,7 @@ protected function parseAddresses($value) */ protected function arrayOfAddresses($address) { - return is_array($address) || - $address instanceof Arrayable || - $address instanceof Traversable; + return is_iterable($address) || $address instanceof Arrayable; } /** @@ -315,9 +312,10 @@ public function render() ); } - return Container::getInstance() - ->make(Markdown::class) - ->render($this->markdown, $this->data()); + $markdown = Container::getInstance()->make(Markdown::class); + + return $markdown->theme($this->theme ?: $markdown->getTheme()) + ->render($this->markdown, $this->data()); } /** @@ -332,4 +330,42 @@ public function withSwiftMessage($callback) return $this; } + + /** + * Apply the callback's message changes if the given "value" is true. + * + * @param mixed $value + * @param callable $callback + * @param callable|null $default + * @return mixed|$this + */ + public function when($value, $callback, $default = null) + { + if ($value) { + return $callback($this, $value) ?: $this; + } elseif ($default) { + return $default($this, $value) ?: $this; + } + + return $this; + } + + /** + * Apply the callback's message changes if the given "value" is false. + * + * @param mixed $value + * @param callable $callback + * @param callable|null $default + * @return mixed|$this + */ + public function unless($value, $callback, $default = null) + { + if (! $value) { + return $callback($this, $value) ?: $this; + } elseif ($default) { + return $default($this, $value) ?: $this; + } + + return $this; + } } diff --git a/src/Illuminate/Notifications/NotificationSender.php b/src/Illuminate/Notifications/NotificationSender.php index 1d6c424b1512..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) @@ -202,7 +202,10 @@ protected function queueNotification($notifiables, $notification) (new SendQueuedNotifications($notifiable, $notification, [$channel])) ->onConnection($notification->connection) ->onQueue($queue) - ->delay($notification->delay) + ->delay(is_array($notification->delay) ? + ($notification->delay[$channel] ?? null) + : $notification->delay + ) ->through( array_merge( method_exists($notification, 'middleware') ? $notification->middleware() : [], 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/Notifications/composer.json b/src/Illuminate/Notifications/composer.json index df6c180a327f..1bc673a22f8c 100644 --- a/src/Illuminate/Notifications/composer.json +++ b/src/Illuminate/Notifications/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "illuminate/broadcasting": "^8.0", "illuminate/bus": "^8.0", "illuminate/collections": "^8.0", diff --git a/src/Illuminate/Pagination/AbstractPaginator.php b/src/Illuminate/Pagination/AbstractPaginator.php index 7b678ab0034c..2894d1f15ffa 100644 --- a/src/Illuminate/Pagination/AbstractPaginator.php +++ b/src/Illuminate/Pagination/AbstractPaginator.php @@ -335,6 +335,19 @@ public function lastItem() return count($this->items) > 0 ? $this->firstItem() + $this->count() - 1 : null; } + /** + * Transform each item in the slice of items using a callback. + * + * @param callable $callback + * @return $this + */ + public function through(callable $callback) + { + $this->items->transform($callback); + + return $this; + } + /** * Get the number of items shown per page. * diff --git a/src/Illuminate/Pagination/LengthAwarePaginator.php b/src/Illuminate/Pagination/LengthAwarePaginator.php index 367c01a0326d..0260b974bc4b 100644 --- a/src/Illuminate/Pagination/LengthAwarePaginator.php +++ b/src/Illuminate/Pagination/LengthAwarePaginator.php @@ -115,11 +115,11 @@ protected function linkCollection() }); })->prepend([ 'url' => $this->previousPageUrl(), - 'label' => 'Previous', + 'label' => function_exists('__') ? __('pagination.previous') : 'Previous', 'active' => false, ])->push([ 'url' => $this->nextPageUrl(), - 'label' => 'Next', + 'label' => function_exists('__') ? __('pagination.next') : 'Next', 'active' => false, ]); } diff --git a/src/Illuminate/Pagination/composer.json b/src/Illuminate/Pagination/composer.json index 30f0b1d04fe4..5c8a380b2a37 100755 --- a/src/Illuminate/Pagination/composer.json +++ b/src/Illuminate/Pagination/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3", + "php": "^7.3|^8.0", "ext-json": "*", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", 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 c4c5a8bc2b5d..2dd4d0ef3389 100644 --- a/src/Illuminate/Pagination/resources/views/tailwind.blade.php +++ b/src/Illuminate/Pagination/resources/views/tailwind.blade.php @@ -1,18 +1,18 @@ @if ($paginator->hasPages()) -