diff --git a/.gitattributes b/.gitattributes index ba7452152c0d..8382fc5c826f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,7 +17,7 @@ .gitattributes export-ignore .gitignore export-ignore .styleci.yml export-ignore -CHANGELOG-* export-ignore +CHANGELOG.md export-ignore CODE_OF_CONDUCT.md export-ignore CONTRIBUTING.md export-ignore docker-compose.yml export-ignore diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 961af6983a22..bd20bfad48bd 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -293,53 +293,53 @@ jobs: DB_USERNAME: SA DB_PASSWORD: Forge123 - # mssql_2017: - # runs-on: ubuntu-20.04 - # timeout-minutes: 5 - - # services: - # sqlsrv: - # image: mcr.microsoft.com/mssql/server:2017-latest - # env: - # ACCEPT_EULA: Y - # SA_PASSWORD: Forge123 - # ports: - # - 1433:1433 - - # strategy: - # fail-fast: true - - # name: SQL Server 2017 - - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - - # - name: Setup PHP - # uses: shivammathur/setup-php@v2 - # with: - # php-version: 8.3 - # extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr - # tools: composer:v2 - # coverage: none - - # - name: Set Framework version - # run: composer config version "12.x-dev" - - # - name: Install dependencies - # uses: nick-fields/retry@v3 - # with: - # timeout_minutes: 5 - # max_attempts: 5 - # command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - - # - name: Execute tests - # run: vendor/bin/phpunit tests/Integration/Database - # env: - # DB_CONNECTION: sqlsrv - # DB_DATABASE: master - # DB_USERNAME: SA - # DB_PASSWORD: Forge123 + mssql_2017: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + services: + sqlsrv: + image: mcr.microsoft.com/mssql/server:2017-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: Forge123 + ports: + - 1433:1433 + + strategy: + fail-fast: true + + name: SQL Server 2017 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr + tools: composer:v2 + coverage: none + + - name: Set Framework version + run: composer config version "12.x-dev" + + - name: Install dependencies + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Database + env: + DB_CONNECTION: sqlsrv + DB_DATABASE: master + DB_USERNAME: SA + DB_PASSWORD: Forge123 sqlite: runs-on: ubuntu-24.04 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a156dd33351..fd2a5f2a7a67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,13 +40,17 @@ jobs: fail-fast: true matrix: php: [8.2, 8.3, 8.4] - phpunit: ['10.5.35', '11.5.3', '12.0.0', '12.1.0'] + phpunit: ['10.5.35', '11.5.3', '12.0.0', '12.2.0'] stability: [prefer-lowest, prefer-stable] exclude: - php: 8.2 phpunit: '12.0.0' - php: 8.2 + phpunit: '12.2.0' + include: + - php: 8.3 phpunit: '12.1.0' + stability: prefer-stable name: PHP ${{ matrix.php }} - PHPUnit ${{ matrix.phpunit }} - ${{ matrix.stability }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0198d02e6505..d81b60f8a0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,179 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.13.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.19.3...12.x) + +## [v12.19.3](https://github.com/laravel/framework/compare/v12.19.2...v12.19.3) - 2025-06-18 + +* [12.x] Fix model pruning when non model files are in the same directory by [@rojtjo](https://github.com/rojtjo) in https://github.com/laravel/framework/pull/56071 + +## [v12.19.2](https://github.com/laravel/framework/compare/v12.19.1...v12.19.2) - 2025-06-17 + +## [v12.19.1](https://github.com/laravel/framework/compare/v12.19.0...v12.19.1) - 2025-06-17 + +* Revert "[12.x] Check if file exists before trying to delete it" by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56072 + +## [v12.19.0](https://github.com/laravel/framework/compare/v12.18.0...v12.19.0) - 2025-06-17 + +* [11.x] Fix validation to not throw incompatible validation exception by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55963 +* [12.x] Correct testEncryptAndDecrypt to properly test new methods by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/55985 +* [12.x] Check if file exists before trying to delete it by [@Jellyfrog](https://github.com/Jellyfrog) in https://github.com/laravel/framework/pull/55994 +* Clear cast caches when discarding changes by [@willtj](https://github.com/willtj) in https://github.com/laravel/framework/pull/55992 +* [12.x] Handle Null Check in Str::contains by [@Jellyfrog](https://github.com/Jellyfrog) in https://github.com/laravel/framework/pull/55991 +* [12.x] Remove call to deprecated `getDefaultDescription` method by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/55990 +* Bump brace-expansion from 2.0.1 to 2.0.2 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55999 +* Enhance error handling in PendingRequest to convert TooManyRedirectsE… by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55998 +* [12.x] fix: remove Model intersection from UserProvider contract by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56013 +* [12.x] Remove the only [@return](https://github.com/return) tag left on a constructor by [@JordanchoEftimov](https://github.com/JordanchoEftimov) in https://github.com/laravel/framework/pull/56001 +* [12.x] Introduce `ComputesOnceableHashInterface` by [@Jacobs63](https://github.com/Jacobs63) in https://github.com/laravel/framework/pull/56009 +* [12.x] Add assertRedirectBackWithErrors to TestResponse by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55987 +* [12.x] collapseWithKeys - Prevent exception in base case by [@DeanWunder](https://github.com/DeanWunder) in https://github.com/laravel/framework/pull/56002 +* [12.x] Standardize size() behavior and add extended queue metrics support by [@sylvesterdamgaard](https://github.com/sylvesterdamgaard) in https://github.com/laravel/framework/pull/56010 +* [11.x] Fix `symfony/console:7.4` compatibility by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56015 +* [12.x] Improve constructor PHPDoc for controller middleware definition by [@JordanchoEftimov](https://github.com/JordanchoEftimov) in https://github.com/laravel/framework/pull/56021 +* Remove `@return` tags from constructors by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/56024 +* [12.x] sort helper functions in alphabetic order by [@gigabites19](https://github.com/gigabites19) in https://github.com/laravel/framework/pull/56031 +* [12.x] add Attachment::fromUploadedFile method by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/56027 +* [12.x]: Add UseEloquentBuilder attribute to register custom Eloquent Builder by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/56025 +* [12.x] Improve PHPDoc for the Illuminate\Cache folder files by [@JordanchoEftimov](https://github.com/JordanchoEftimov) in https://github.com/laravel/framework/pull/56028 +* [12.x] Add a new model cast named asFluent by [@azim-kordpour](https://github.com/azim-kordpour) in https://github.com/laravel/framework/pull/56046 +* [12.x] Introduce `FailOnException` job middleware by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56037 +* [12.x] isSoftDeletable(), isPrunable(), and isMassPrunable() to model class by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56060 + +## [v12.18.0](https://github.com/laravel/framework/compare/v12.17.0...v12.18.0) - 2025-06-10 + +* document `through()` method in interfaces to fix IDE warnings by [@harryqt](https://github.com/harryqt) in https://github.com/laravel/framework/pull/55925 +* [12.x] Add encrypt and decrypt Str helper methods by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/55931 +* [12.x] Add a command option for making batchable jobs by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/55929 +* [12.x] fix: intersect Authenticatable with Model in UserProvider phpdocs by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/54061 +* [12.x] feat: create UsePolicy attribute by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55882 +* [12.x] `ScheduledTaskFailed` not dispatched on scheduled forground task fails by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55624 +* [12.x] Add generics to `Model::unguarded()` by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/55932 +* [12.x] Fix SSL Certificate and Connection Errors Leaking as Guzzle Exceptions by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55937 +* Fix deprecation warning in PHP 8.3 by ensuring string type in explode() by [@Khuthaily](https://github.com/Khuthaily) in https://github.com/laravel/framework/pull/55939 +* revert: #55939 by [@NickSdot](https://github.com/NickSdot) in https://github.com/laravel/framework/pull/55943 +* [12.x] feat: Add WorkerStarting event when worker daemon starts by [@Orrison](https://github.com/Orrison) in https://github.com/laravel/framework/pull/55941 +* [12.x] Allow setting the `RequestException` truncation limit per request by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55897 +* [12.x] feat: Make custom eloquent castings comparable for more granular isDirty check by [@SanderSander](https://github.com/SanderSander) in https://github.com/laravel/framework/pull/55945 +* [12.x] fix alphabetical order by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55965 +* [12.x] Use native named parameter instead of unused variable by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55964 +* [12.x] add generics to Model attribute related methods and properties by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55962 +* [12.x] Supports PHPUnit 12.2 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55961 +* [12.x] feat: Add ability to override SendQueuedNotifications job class by [@Orrison](https://github.com/Orrison) in https://github.com/laravel/framework/pull/55942 +* [12.x] Fix timezone validation test for PHP 8.3+ by [@platoindebugmode](https://github.com/platoindebugmode) in https://github.com/laravel/framework/pull/55956 +* Broadcasting Utilities by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55967 +* [12.x] Remove unused $guarded parameter from testChannelNameNormalization method by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55973 +* [12.x] Validate that `outOf` is greater than 0 in `Lottery` helper by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/55969 +* [12.x] Allow retrieving all reported exceptions from `ExceptionHandlerFake` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55972 + +## [v12.17.0](https://github.com/laravel/framework/compare/v12.16.0...v12.17.0) - 2025-06-03 + +* [11.x] Backport `TestResponse::assertRedirectBack` by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/55780 +* Add support for sending raw (non-encoded) attachments in Resend mail by [@Roywcm](https://github.com/Roywcm) in https://github.com/laravel/framework/pull/55837 +* [12.x] chore: return Collection from timestamps methods by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55871 +* [12.x] fix: fully qualify collection return type by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55873 +* [12.x] Fix Blade nested default component resolution for custom namespaces by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55874 +* [12.x] Fix return types in console command handlers to void by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55876 +* [12.x] Ability to perform higher order static calls on collection items by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55880 +* Adds Resource helpers to cursor paginator by [@jsandfordhughescoop](https://github.com/jsandfordhughescoop) in https://github.com/laravel/framework/pull/55879 +* Add reorderDesc() to Query Builder by [@ghabriel25](https://github.com/ghabriel25) in https://github.com/laravel/framework/pull/55885 +* [11.x] Fixes Symfony Console 7.3 deprecations on closure command by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55888 +* [12.x] Add `AsUri` model cast by [@ash-jc-allen](https://github.com/ash-jc-allen) in https://github.com/laravel/framework/pull/55909 +* [12.x] feat: Add Contextual Implementation/Interface Binding via PHP8 Attribute by [@yitzwillroth](https://github.com/yitzwillroth) in https://github.com/laravel/framework/pull/55904 +* [12.x] Add tests for the `AuthenticateSession` Middleware by [@imanghafoori1](https://github.com/imanghafoori1) in https://github.com/laravel/framework/pull/55900 +* [12.x] Allow brick/math ^0.13 by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/54964 +* [12.x] fix: Factory::state and ::prependState generics by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55915 + +## [v12.16.0](https://github.com/laravel/framework/compare/v12.15.0...v12.16.0) - 2025-05-27 + +* [12.x] Change priority in optimize:clear by [@amirmohammadnajmi](https://github.com/amirmohammadnajmi) in https://github.com/laravel/framework/pull/55792 +* [12.x] Fix `TestResponse::assertSessionMissing()` when given an array of keys by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55800 +* [12.x] Allowing `Context` Attribute to Interact with Hidden by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55799 +* Add support for sending raw (non-encoded) attachments in Resend mail driver by [@Roywcm](https://github.com/Roywcm) in https://github.com/laravel/framework/pull/55803 +* [12.x] Added option to always defer for flexible cache by [@Zwartpet](https://github.com/Zwartpet) in https://github.com/laravel/framework/pull/55802 +* [12.x] style: Use null coalescing assignment (??=) for cleaner code by [@mohsenetm](https://github.com/mohsenetm) in https://github.com/laravel/framework/pull/55823 +* [12.x] Introducing `Arr::hasAll` by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55815 +* [12.x] Restore lazy loading check by [@decadence](https://github.com/decadence) in https://github.com/laravel/framework/pull/55817 +* [12.x] Minor language update by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55812 +* fix(cache/redis): use connectionAwareSerialize in RedisStore::putMany() by [@superbiche](https://github.com/superbiche) in https://github.com/laravel/framework/pull/55814 +* [12.x] Fix `ResponseFactory` should also accept `null` callback by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55833 +* [12.x] Add template variables to scope by [@wietsewarendorff](https://github.com/wietsewarendorff) in https://github.com/laravel/framework/pull/55830 +* [12.x] Introducing `toUri` to the `Stringable` Class by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55862 +* [12.x] Remove remaining [@return](https://github.com/return) tags from constructors by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55858 +* [12.x] Replace alias `is_integer()` with `is_int()` to comply with Laravel Pint by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55851 +* Fix argument types for Illuminate/Database/Query/Builder::upsert() by [@jellisii](https://github.com/jellisii) in https://github.com/laravel/framework/pull/55849 +* [12.x] Add `in_array_keys` validation rule to check for presence of specified array keys by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/55807 +* [12.x] Add `Rule::contains` by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/55809 + +## [v12.15.0](https://github.com/laravel/framework/compare/v12.14.1...v12.15.0) - 2025-05-20 + +* [12.x] Add locale-aware number parsing methods to Number class by [@informagenie](https://github.com/informagenie) in https://github.com/laravel/framework/pull/55725 +* [12.x] Add a default option when retrieving an enum from data by [@elbojoloco](https://github.com/elbojoloco) in https://github.com/laravel/framework/pull/55735 +* Revert "[12.x] Update "Number::fileSize" to use correct prefix and add prefix param" by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/55741 +* [12.x] Remove apc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55745 +* [12.x] Add param type for `assertJsonStructure` & `assertExactJsonStructure` methods by [@milwad-dev](https://github.com/milwad-dev) in https://github.com/laravel/framework/pull/55743 +* [12.x] Fix type casting for environment variables in config files by [@adamwhp](https://github.com/adamwhp) in https://github.com/laravel/framework/pull/55737 +* [12.x] Preserve "previous" model state by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55729 +* [12.x] Passthru `getCountForPagination` on an Eloquent\Builder by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55752 +* [12.x] Add `assertClientError` method to `TestResponse` by [@shane-zeng](https://github.com/shane-zeng) in https://github.com/laravel/framework/pull/55750 +* Install Broadcasting Command Fix for Livewire Starter Kit by [@joshcirre](https://github.com/joshcirre) in https://github.com/laravel/framework/pull/55774 +* Clarify units for benchmark value for IDE accessibility by [@mike-healy](https://github.com/mike-healy) in https://github.com/laravel/framework/pull/55781 +* Improved PHPDoc Return Types for Eloquent's Original Attribute Methods by [@clementbirkle](https://github.com/clementbirkle) in https://github.com/laravel/framework/pull/55779 +* [12.x] Prevent `preventsLazyLoading` exception when using `automaticallyEagerLoadRelationships` by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/55771 +* [12.x] Add `hash` string helper by [@istiak-tridip](https://github.com/istiak-tridip) in https://github.com/laravel/framework/pull/55767 +* [12.x] Update `assertSessionMissing()` signature to match `assertSessionHas()` by [@nexxai](https://github.com/nexxai) in https://github.com/laravel/framework/pull/55763 +* Fix: php artisan db command if no password by [@mr-chetan](https://github.com/mr-chetan) in https://github.com/laravel/framework/pull/55761 +* [12.x] Types: InteractsWithPivotTable::sync by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55762 +* [12.x] feat: Add `current_page_url` to Paginator by [@mariomka](https://github.com/mariomka) in https://github.com/laravel/framework/pull/55789 +* Correct return type in PhpDoc for command fail method by [@Muetze42](https://github.com/Muetze42) in https://github.com/laravel/framework/pull/55783 +* [12.x] Add `assertRedirectToAction` method to test redirection to controller actions by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55788 +* [12.x] Add Context contextual attribute by [@martinbean](https://github.com/martinbean) in https://github.com/laravel/framework/pull/55760 + +## [v12.14.1](https://github.com/laravel/framework/compare/v12.14.0...v12.14.1) - 2025-05-13 + +* [10.x] Refine error messages for detecting lost connections (Debian bookworm compatibility) by [@mfn](https://github.com/mfn) in https://github.com/laravel/framework/pull/53794 +* [10.x] Bump minimum `league/commonmark` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/53829 +* [10.x] Backport 11.x PHP 8.4 fix for str_getcsv deprecation by [@aka-tpayne](https://github.com/aka-tpayne) in https://github.com/laravel/framework/pull/54074 +* [10.x] Fix attribute name used on `Validator` instance within certain rule classes by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54943 +* Add `Illuminate\Support\EncodedHtmlString` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54737 +* [11.x] Fix missing `return $this` for `assertOnlyJsonValidationErrors` by [@LeTamanoir](https://github.com/LeTamanoir) in https://github.com/laravel/framework/pull/55099 +* [11.x] Fix `Illuminate\Support\EncodedHtmlString` from causing breaking change by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55149 +* [11.x] Respect custom path for cached views by the `AboutCommand` by [@alies-dev](https://github.com/alies-dev) in https://github.com/laravel/framework/pull/55179 +* [11.x] Include all invisible characters in Str::trim by [@laserhybiz](https://github.com/laserhybiz) in https://github.com/laravel/framework/pull/54281 +* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55302 +* [11.x] Remove incorrect syntax from mail's `message` template by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55530 +* [11.x] Allows to toggle markdown email encoding by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55539 +* [11.x] Fix `EncodedHtmlString` to ignore instance of `HtmlString` by [@jbraband](https://github.com/jbraband) in https://github.com/laravel/framework/pull/55543 +* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55549 +* [11.x] Install Passport 13.x by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/55621 +* [11.x] Bump minimum league/commonmark by [@andrextor](https://github.com/andrextor) in https://github.com/laravel/framework/pull/55660 +* Backporting Timebox fixes to 11.x by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/55705 +* Test SQLServer 2017 on Ubuntu 22.04 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55716 +* [11.x] Fix Symfony 7.3 deprecations by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/55711 +* Easily implement broadcasting in a React/Vue Typescript app (Starter Kits) by [@tnylea](https://github.com/tnylea) in https://github.com/laravel/framework/pull/55170 + +## [v12.14.0](https://github.com/laravel/framework/compare/v12.13.0...v12.14.0) - 2025-05-13 + +* [12.x] Support `useCurrent` on date and year column types by [@nicholasbrantley](https://github.com/nicholasbrantley) in https://github.com/laravel/framework/pull/55619 +* [12.x] Update "Number::fileSize" to use correct prefix and add prefix param by [@Boy132](https://github.com/Boy132) in https://github.com/laravel/framework/pull/55678 +* [12.x] Update PHPDoc for whereRaw to allow Expression as $sql by [@mitoop](https://github.com/mitoop) in https://github.com/laravel/framework/pull/55674 +* Revert "[12.x] Make Blueprint Resolver Statically" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55690 +* [12.x] Support Virtual Properties When Serializing Models by [@beschoenen](https://github.com/beschoenen) in https://github.com/laravel/framework/pull/55691 +* [12.X] Fix `Http::preventStrayRequests` error propagation when using `Http::pool` by [@LeTamanoir](https://github.com/LeTamanoir) in https://github.com/laravel/framework/pull/55689 +* [12.x] incorrect use of generics in Schema\Builder by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55687 +* [12.x] Add option to disable MySQL ssl when restoring or squashing migrations by [@andersonls](https://github.com/andersonls) in https://github.com/laravel/framework/pull/55683 +* [12.x] Add `except` and `exceptHidden` methods to `Context` class by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55692 +* [12.x] Container `currentlyResolving` utility by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/55684 +* [12.x] Container `currentlyResolving` test by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/55694 +* [12.x] Fix handling of default values for route parameters with a binding field by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/55697 +* Move Timebox for Authentication and add to password resets by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/55701 +* [12.x] perf: Optimize BladeCompiler by [@rzv-me](https://github.com/rzv-me) in https://github.com/laravel/framework/pull/55703 +* [12.x] perf: support iterables for event discovery paths by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55699 +* [12.x] Types: AuthorizesRequests::resourceAbilityMap by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55706 +* [12.x] Add flexible support to memoized cache store by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55709 +* [12.x] Introduce Arr::from() by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55715 +* [12.x] Fix the `getCurrentlyAttachedPivots` wrong `morphClass` for morph to many relationships by [@amir9480](https://github.com/amir9480) in https://github.com/laravel/framework/pull/55721 +* [12.x] Improve typehints for Http classes by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54783 +* Add deleteWhen for throttle exceptions job middleware by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/55718 ## [v12.13.0](https://github.com/laravel/framework/compare/v12.12.0...v12.13.0) - 2025-05-07 diff --git a/composer.json b/composer.json index 8c76eb8b7c06..7bebb6b83513 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "ext-session": "*", "ext-tokenizer": "*", "composer-runtime-api": "^2.2", - "brick/math": "^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", "egulias/email-validator": "^3.2.1|^4.0", diff --git a/config/app.php b/config/app.php index 16073173f8f8..1ced8bef0a14 100644 --- a/config/app.php +++ b/config/app.php @@ -130,7 +130,7 @@ 'previous_keys' => [ ...array_filter( - explode(',', env('APP_PREVIOUS_KEYS', '')) + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) ), ], diff --git a/config/auth.php b/config/auth.php index 0ba5d5d8f10c..7d1eb0de5f7b 100644 --- a/config/auth.php +++ b/config/auth.php @@ -104,7 +104,7 @@ | Password Confirmation Timeout |-------------------------------------------------------------------------- | - | Here you may define the amount of seconds before a password confirmation + | Here you may define the number of seconds before a password confirmation | window expires and users are asked to re-enter their password via the | confirmation screen. By default, the timeout lasts for three hours. | diff --git a/config/cache.php b/config/cache.php index 925f7d2ee84b..f529e1e3ec74 100644 --- a/config/cache.php +++ b/config/cache.php @@ -103,6 +103,6 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel'), '_').'_cache_'), ]; diff --git a/config/database.php b/config/database.php index 3e827c359b04..8a3b731fb52e 100644 --- a/config/database.php +++ b/config/database.php @@ -148,7 +148,7 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel'), '_').'_database_'), 'persistent' => env('REDIS_PERSISTENT', false), ], diff --git a/config/logging.php b/config/logging.php index 1345f6f66c51..9e998a496c86 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,7 +54,7 @@ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', 'single')), + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], diff --git a/config/mail.php b/config/mail.php index ff140eb439f8..22c03b032d76 100644 --- a/config/mail.php +++ b/config/mail.php @@ -46,7 +46,7 @@ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], 'ses' => [ diff --git a/config/services.php b/config/services.php index 27a36175f823..6182e4b90c94 100644 --- a/config/services.php +++ b/config/services.php @@ -18,16 +18,16 @@ 'token' => env('POSTMARK_TOKEN'), ], + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + 'ses' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], - 'resend' => [ - 'key' => env('RESEND_KEY'), - ], - 'slack' => [ 'notifications' => [ 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), diff --git a/config/session.php b/config/session.php index ba0aa60b074b..13d86a4ac63d 100644 --- a/config/session.php +++ b/config/session.php @@ -13,8 +13,8 @@ | incoming requests. Laravel supports a variety of storage options to | persist session data. Database storage is a great default choice. | - | Supported: "file", "cookie", "database", "apc", - | "memcached", "redis", "dynamodb", "array" + | Supported: "file", "cookie", "database", "memcached", + | "redis", "dynamodb", "array" | */ @@ -97,7 +97,7 @@ | define the cache store which should be used to store the session data | between requests. This must match one of your defined cache stores. | - | Affects: "apc", "dynamodb", "memcached", "redis" + | Affects: "dynamodb", "memcached", "redis" | */ @@ -129,7 +129,7 @@ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + Str::slug((string) env('APP_NAME', 'laravel'), '_').'_session' ), /* diff --git a/src/Illuminate/Auth/Access/Gate.php b/src/Illuminate/Auth/Access/Gate.php index 47dea0ddd26e..361fa2c7a043 100644 --- a/src/Illuminate/Auth/Access/Gate.php +++ b/src/Illuminate/Auth/Access/Gate.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Auth\Access\Gate as GateContract; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -669,6 +670,12 @@ public function getPolicyFor($class) return $this->resolvePolicy($this->policies[$class]); } + $policy = $this->getPolicyFromAttribute($class); + + if (! is_null($policy)) { + return $this->resolvePolicy($policy); + } + foreach ($this->guessPolicyName($class) as $guessedPolicy) { if (class_exists($guessedPolicy)) { return $this->resolvePolicy($guessedPolicy); @@ -682,6 +689,25 @@ public function getPolicyFor($class) } } + /** + * Get the policy class from the class attribute. + * + * @param class-string<*> $class + * @return class-string<*>|null + */ + protected function getPolicyFromAttribute(string $class): ?string + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new ReflectionClass($class))->getAttributes(UsePolicy::class); + + return $attributes !== [] + ? $attributes[0]->newInstance()->class + : null; + } + /** * Guess the policy name for the given class. * diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index 131959148b06..8c12db570ae4 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -66,7 +66,7 @@ public function guard($name = null) { $name = $name ?: $this->getDefaultDriver(); - return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name); + return $this->guards[$name] ??= $this->resolve($name); } /** diff --git a/src/Illuminate/Auth/EloquentUserProvider.php b/src/Illuminate/Auth/EloquentUserProvider.php index e91f1057b553..1bb42edc6ce4 100755 --- a/src/Illuminate/Auth/EloquentUserProvider.php +++ b/src/Illuminate/Auth/EloquentUserProvider.php @@ -20,7 +20,7 @@ class EloquentUserProvider implements UserProvider /** * The Eloquent user model. * - * @var string + * @var class-string<\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model> */ protected $model; @@ -47,7 +47,7 @@ public function __construct(HasherContract $hasher, $model) * Retrieve a user by their unique identifier. * * @param mixed $identifier - * @return \Illuminate\Contracts\Auth\Authenticatable|null + * @return (\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model)|null */ public function retrieveById($identifier) { @@ -63,7 +63,7 @@ public function retrieveById($identifier) * * @param mixed $identifier * @param string $token - * @return \Illuminate\Contracts\Auth\Authenticatable|null + * @return (\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model)|null */ public function retrieveByToken($identifier, #[\SensitiveParameter] $token) { @@ -85,7 +85,7 @@ public function retrieveByToken($identifier, #[\SensitiveParameter] $token) /** * Update the "remember me" token for the given user in storage. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param \Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model $user * @param string $token * @return void */ @@ -106,7 +106,7 @@ public function updateRememberToken(UserContract $user, #[\SensitiveParameter] $ * Retrieve a user by the given credentials. * * @param array $credentials - * @return \Illuminate\Contracts\Auth\Authenticatable|null + * @return (\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model)|null */ public function retrieveByCredentials(#[\SensitiveParameter] array $credentials) { @@ -161,7 +161,7 @@ public function validateCredentials(UserContract $user, #[\SensitiveParameter] a /** * Rehash the user's password if required and supported. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param \Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model $user * @param array $credentials * @param bool $force * @return void @@ -199,7 +199,7 @@ protected function newModelQuery($model = null) /** * Create a new instance of the model. * - * @return \Illuminate\Database\Eloquent\Model + * @return \Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model */ public function createModel() { @@ -234,7 +234,7 @@ public function setHasher(HasherContract $hasher) /** * Gets the name of the Eloquent user model. * - * @return string + * @return class-string<\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model> */ public function getModel() { @@ -244,7 +244,7 @@ public function getModel() /** * Sets the name of the Eloquent user model. * - * @param string $model + * @param class-string<\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model> $model * @return $this */ public function setModel($model) diff --git a/src/Illuminate/Auth/Passwords/CacheTokenRepository.php b/src/Illuminate/Auth/Passwords/CacheTokenRepository.php index 4fb7c67ae16b..0ea37eef06ef 100644 --- a/src/Illuminate/Auth/Passwords/CacheTokenRepository.php +++ b/src/Illuminate/Auth/Passwords/CacheTokenRepository.php @@ -24,7 +24,6 @@ public function __construct( protected string $hashKey, protected int $expires = 3600, protected int $throttle = 60, - protected string $prefix = '', ) { } @@ -41,7 +40,7 @@ public function create(CanResetPasswordContract $user) $token = hash_hmac('sha256', Str::random(40), $this->hashKey); $this->cache->put( - $this->prefix.$user->getEmailForPasswordReset(), + $this->cacheKey($user), [$this->hasher->make($token), Carbon::now()->format($this->format)], $this->expires, ); @@ -58,7 +57,7 @@ public function create(CanResetPasswordContract $user) */ public function exists(CanResetPasswordContract $user, #[\SensitiveParameter] $token) { - [$record, $createdAt] = $this->cache->get($this->prefix.$user->getEmailForPasswordReset()); + [$record, $createdAt] = $this->cache->get($this->cacheKey($user)); return $record && ! $this->tokenExpired($createdAt) @@ -84,7 +83,7 @@ protected function tokenExpired($createdAt) */ public function recentlyCreatedToken(CanResetPasswordContract $user) { - [$record, $createdAt] = $this->cache->get($this->prefix.$user->getEmailForPasswordReset()); + [$record, $createdAt] = $this->cache->get($this->cacheKey($user)); return $record && $this->tokenRecentlyCreated($createdAt); } @@ -114,7 +113,7 @@ protected function tokenRecentlyCreated($createdAt) */ public function delete(CanResetPasswordContract $user) { - $this->cache->forget($this->prefix.$user->getEmailForPasswordReset()); + $this->cache->forget($this->cacheKey($user)); } /** @@ -125,4 +124,15 @@ public function delete(CanResetPasswordContract $user) public function deleteExpired() { } + + /** + * Determine the cache key for the given user. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * @return string + */ + public function cacheKey(CanResetPasswordContract $user): string + { + return hash('sha256', $user->getEmailForPasswordReset()); + } } diff --git a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php index 3946e596ef9f..6e42bba190d8 100644 --- a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php +++ b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php @@ -95,7 +95,6 @@ protected function createTokenRepository(array $config) $key, ($config['expire'] ?? 60) * 60, $config['throttle'] ?? 0, - $config['prefix'] ?? '', ); } diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index 790e096bbaa2..8f653549a9a0 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -14,6 +14,7 @@ use Illuminate\Contracts\Broadcasting\Factory as FactoryContract; use Illuminate\Contracts\Broadcasting\ShouldBeUnique; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; +use Illuminate\Contracts\Broadcasting\ShouldRescue; use Illuminate\Contracts\Bus\Dispatcher as BusDispatcherContract; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Foundation\CachesRoutes; @@ -178,7 +179,12 @@ public function queue($event) (is_object($event) && method_exists($event, 'shouldBroadcastNow') && $event->shouldBroadcastNow())) { - return $this->app->make(BusDispatcherContract::class)->dispatchNow(new BroadcastEvent(clone $event)); + $dispatch = fn () => $this->app->make(BusDispatcherContract::class) + ->dispatchNow(new BroadcastEvent(clone $event)); + + return $event instanceof ShouldRescue + ? $this->rescue($dispatch) + : $dispatch(); } $queue = null; @@ -201,9 +207,13 @@ public function queue($event) } } - $this->app->make('queue') + $push = fn () => $this->app->make('queue') ->connection($event->connection ?? null) ->pushOn($queue, $broadcastEvent); + + $event instanceof ShouldRescue + ? $this->rescue($push) + : $push(); } /** @@ -475,6 +485,21 @@ public function extend($driver, Closure $callback) return $this; } + /** + * Execute the given callback using "rescue" if possible. + * + * @param \Closure $callback + * @return mixed + */ + protected function rescue(Closure $callback) + { + if (function_exists('rescue')) { + return rescue($callback); + } + + return $callback(); + } + /** * Get the application instance used by the manager. * diff --git a/src/Illuminate/Broadcasting/FakePendingBroadcast.php b/src/Illuminate/Broadcasting/FakePendingBroadcast.php new file mode 100644 index 000000000000..769a213dd99a --- /dev/null +++ b/src/Illuminate/Broadcasting/FakePendingBroadcast.php @@ -0,0 +1,45 @@ +connection->table($this->table) ->orderByDesc('id') - ->take($limit) + ->limit($limit) ->when($before, fn ($q) => $q->where('id', '<', $before)) ->get() ->map(function ($batch) { @@ -247,7 +247,7 @@ public function prune(DateTimeInterface $before) $totalDeleted = 0; do { - $deleted = $query->take(1000)->delete(); + $deleted = $query->limit(1000)->delete(); $totalDeleted += $deleted; } while ($deleted !== 0); @@ -270,7 +270,7 @@ public function pruneUnfinished(DateTimeInterface $before) $totalDeleted = 0; do { - $deleted = $query->take(1000)->delete(); + $deleted = $query->limit(1000)->delete(); $totalDeleted += $deleted; } while ($deleted !== 0); @@ -293,7 +293,7 @@ public function pruneCancelled(DateTimeInterface $before) $totalDeleted = 0; do { - $deleted = $query->take(1000)->delete(); + $deleted = $query->limit(1000)->delete(); $totalDeleted += $deleted; } while ($deleted !== 0); diff --git a/src/Illuminate/Cache/ArrayLock.php b/src/Illuminate/Cache/ArrayLock.php index 2eb5054dd544..3252cb2ffdf5 100644 --- a/src/Illuminate/Cache/ArrayLock.php +++ b/src/Illuminate/Cache/ArrayLock.php @@ -82,7 +82,7 @@ public function release() /** * Returns the owner value written into the driver for this lock. * - * @return string + * @return string|null */ protected function getCurrentOwner() { diff --git a/src/Illuminate/Cache/DatabaseLock.php b/src/Illuminate/Cache/DatabaseLock.php index 8e63374cb988..d490f8c05048 100644 --- a/src/Illuminate/Cache/DatabaseLock.php +++ b/src/Illuminate/Cache/DatabaseLock.php @@ -44,6 +44,7 @@ class DatabaseLock extends Lock * @param int $seconds * @param string|null $owner * @param array $lottery + * @param int $defaultTimeoutInSeconds */ public function __construct(Connection $connection, $table, $name, $seconds, $owner = null, $lottery = [2, 100], $defaultTimeoutInSeconds = 86400) { diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 04c52e45922d..2d5fd6d03b92 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -76,6 +76,7 @@ class DatabaseStore implements LockProvider, Store * @param string $prefix * @param string $lockTable * @param array $lockLottery + * @param int $defaultLockTimeoutInSeconds */ public function __construct( ConnectionInterface $connection, @@ -169,6 +170,7 @@ public function put($key, $value, $seconds) /** * Store multiple items in the cache for a given number of seconds. * + * @param array $values * @param int $seconds * @return bool */ diff --git a/src/Illuminate/Cache/Events/CacheFlushFailed.php b/src/Illuminate/Cache/Events/CacheFlushFailed.php index 7d987e9de82c..7df29a0f96e1 100644 --- a/src/Illuminate/Cache/Events/CacheFlushFailed.php +++ b/src/Illuminate/Cache/Events/CacheFlushFailed.php @@ -22,7 +22,7 @@ class CacheFlushFailed * Create a new event instance. * * @param string|null $storeName - * @return void + * @param array $tags */ public function __construct($storeName, array $tags = []) { diff --git a/src/Illuminate/Cache/Events/CacheFlushed.php b/src/Illuminate/Cache/Events/CacheFlushed.php index 5f942afdd1af..01e781cbb879 100644 --- a/src/Illuminate/Cache/Events/CacheFlushed.php +++ b/src/Illuminate/Cache/Events/CacheFlushed.php @@ -22,7 +22,7 @@ class CacheFlushed * Create a new event instance. * * @param string|null $storeName - * @return void + * @param array $tags */ public function __construct($storeName, array $tags = []) { diff --git a/src/Illuminate/Cache/Events/CacheFlushing.php b/src/Illuminate/Cache/Events/CacheFlushing.php index 905f016143d7..4cf0c455dcca 100644 --- a/src/Illuminate/Cache/Events/CacheFlushing.php +++ b/src/Illuminate/Cache/Events/CacheFlushing.php @@ -22,7 +22,7 @@ class CacheFlushing * Create a new event instance. * * @param string|null $storeName - * @return void + * @param array $tags */ public function __construct($storeName, array $tags = []) { diff --git a/src/Illuminate/Cache/MemoizedStore.php b/src/Illuminate/Cache/MemoizedStore.php index fc6313db2a1a..6c24e33346ce 100644 --- a/src/Illuminate/Cache/MemoizedStore.php +++ b/src/Illuminate/Cache/MemoizedStore.php @@ -108,6 +108,7 @@ public function put($key, $value, $seconds) /** * Store multiple items in the cache for a given number of seconds. * + * @param array $values * @param int $seconds * @return bool */ diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 33cdf87307c7..399f4ac78ea0 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -140,7 +140,7 @@ public function putMany(array $values, $seconds) $serializedValues = []; foreach ($values as $key => $value) { - $serializedValues[$this->prefix.$key] = $this->serialize($value); + $serializedValues[$this->prefix.$key] = $this->connectionAwareSerialize($value, $connection); } $connection->multi(); diff --git a/src/Illuminate/Cache/RedisTagSet.php b/src/Illuminate/Cache/RedisTagSet.php index 267c11607cd4..88cb4a753ad3 100644 --- a/src/Illuminate/Cache/RedisTagSet.php +++ b/src/Illuminate/Cache/RedisTagSet.php @@ -90,6 +90,7 @@ public function flushStaleEntries() * Flush the tag from the cache. * * @param string $name + * @return string */ public function flushTag($name) { diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 3eb6f700ed01..5b55da8e3008 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -483,9 +483,10 @@ public function rememberForever($key, Closure $callback) * @param array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int } $ttl * @param (callable(): TCacheValue) $callback * @param array{ seconds?: int, owner?: string }|null $lock + * @param bool $alwaysDefer * @return TCacheValue */ - public function flexible($key, $ttl, $callback, $lock = null) + public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = false) { [ $key => $value, @@ -520,7 +521,7 @@ public function flexible($key, $ttl, $callback, $lock = null) }); }; - defer($refresh, "illuminate:cache:flexible:{$key}"); + defer($refresh, "illuminate:cache:flexible:{$key}", $alwaysDefer); return $value; } diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index bea43ce76c26..e499ef7c6811 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -4,6 +4,7 @@ use ArgumentCountError; use ArrayAccess; +use Closure; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\Macroable; @@ -495,6 +496,30 @@ public static function has($array, $keys) return true; } + /** + * Determine if all keys exist in an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string|array $keys + * @return bool + */ + public static function hasAll($array, $keys) + { + $keys = (array) $keys; + + if (! $array || $keys === []) { + return false; + } + + foreach ($keys as $key) { + if (! static::has($array, $key)) { + return false; + } + } + + return true; + } + /** * Determine if any of the keys exist in an array using "dot" notation. * @@ -534,7 +559,7 @@ public static function integer(ArrayAccess|array $array, string|int|null $key, ? { $value = Arr::get($array, $key, $default); - if (! is_integer($value)) { + if (! is_int($value)) { throw new InvalidArgumentException( sprintf('Array value for key [%s] must be an integer, %s found.', $key, gettype($value)) ); @@ -662,8 +687,8 @@ public static function select($array, $keys) * Pluck an array of values from an array. * * @param iterable $array - * @param string|array|int|null $value - * @param string|array|null $key + * @param string|array|int|Closure|null $value + * @param string|array|Closure|null $key * @return array */ public static function pluck($array, $value, $key = null) @@ -673,7 +698,9 @@ public static function pluck($array, $value, $key = null) [$value, $key] = static::explodePluckParameters($value, $key); foreach ($array as $item) { - $itemValue = data_get($item, $value); + $itemValue = $value instanceof Closure + ? $value($item) + : data_get($item, $value); // If the key is "null", we will just append the value to the array and keep // looping. Otherwise we will key the array using the value of the key we @@ -681,7 +708,9 @@ public static function pluck($array, $value, $key = null) if (is_null($key)) { $results[] = $itemValue; } else { - $itemKey = data_get($item, $key); + $itemKey = $key instanceof Closure + ? $key($item) + : data_get($item, $key); if (is_object($itemKey) && method_exists($itemKey, '__toString')) { $itemKey = (string) $itemKey; @@ -697,15 +726,15 @@ public static function pluck($array, $value, $key = null) /** * Explode the "value" and "key" arguments passed to "pluck". * - * @param string|array $value - * @param string|array|null $key + * @param string|array|Closure $value + * @param string|array|Closure|null $key * @return array */ protected static function explodePluckParameters($value, $key) { $value = is_string($value) ? explode('.', $value) : $value; - $key = is_null($key) || is_array($key) ? $key : explode('.', $key); + $key = is_null($key) || is_array($key) || $key instanceof Closure ? $key : explode('.', $key); return [$value, $key]; } @@ -924,10 +953,10 @@ public static function shuffle($array) } /** - * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. + * Get the first item in the array, but only if exactly one item exists. Otherwise, throw an exception. * * @param array $array - * @param callable $callback + * @param (callable(mixed, array-key): array)|null $callback * * @throws \Illuminate\Support\ItemNotFoundException * @throws \Illuminate\Support\MultipleItemsFoundException @@ -1035,7 +1064,7 @@ public static function string(ArrayAccess|array $array, string|int|null $key, ?s /** * Conditionally compile classes from an array into a CSS class list. * - * @param array $array + * @param array|string $array * @return string */ public static function toCssClasses($array) @@ -1058,7 +1087,7 @@ public static function toCssClasses($array) /** * Conditionally compile styles from an array into a style list. * - * @param array $array + * @param array|string $array * @return string */ public static function toCssStyles($array) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 95faa17a7121..2ffeefbc53aa 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -165,6 +165,10 @@ public function collapseWithKeys() $results[$key] = $values; } + if (! $results) { + return new static; + } + return new static(array_replace(...$results)); } @@ -1232,7 +1236,7 @@ public function after($value, $strict = false) /** * Get and remove the first N items from the collection. * - * @param int $count + * @param int<0, max> $count * @return static|TValue|null * * @throws \InvalidArgumentException @@ -1725,8 +1729,10 @@ public function takeWhile($value) /** * Transform each item in the collection using a callback. * - * @param callable(TValue, TKey): TValue $callback - * @return $this + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return $this */ public function transform(callable $callback) { @@ -1838,7 +1844,7 @@ public function getIterator(): Traversable /** * Count the number of items in the collection. * - * @return int + * @return int<0, max> */ public function count(): int { diff --git a/src/Illuminate/Collections/HigherOrderCollectionProxy.php b/src/Illuminate/Collections/HigherOrderCollectionProxy.php index 7edfd4fa2c3b..035d0fda4d58 100644 --- a/src/Illuminate/Collections/HigherOrderCollectionProxy.php +++ b/src/Illuminate/Collections/HigherOrderCollectionProxy.php @@ -61,7 +61,9 @@ public function __get($key) public function __call($method, $parameters) { return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { - return $value->{$method}(...$parameters); + return is_string($value) + ? $value::{$method}(...$parameters) + : $value->{$method}(...$parameters); }); } } diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index daf811bfcadd..48e436c7335c 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -777,12 +777,16 @@ public function pluck($value, $key = null) [$value, $key] = $this->explodePluckParameters($value, $key); foreach ($this as $item) { - $itemValue = data_get($item, $value); + $itemValue = $value instanceof Closure + ? $value($item) + : data_get($item, $value); if (is_null($key)) { yield $itemValue; } else { - $itemKey = data_get($item, $key); + $itemKey = $key instanceof Closure + ? $key($item) + : data_get($item, $key); if (is_object($itemKey) && method_exists($itemKey, '__toString')) { $itemKey = (string) $itemKey; @@ -1869,7 +1873,7 @@ protected function explodePluckParameters($value, $key) { $value = is_string($value) ? explode('.', $value) : $value; - $key = is_null($key) || is_array($key) ? $key : explode('.', $key); + $key = is_null($key) || is_array($key) || $key instanceof Closure ? $key : explode('.', $key); return [$value, $key]; } diff --git a/src/Illuminate/Config/Repository.php b/src/Illuminate/Config/Repository.php index 19240b42ac93..08801213a0f3 100644 --- a/src/Illuminate/Config/Repository.php +++ b/src/Illuminate/Config/Repository.php @@ -5,6 +5,7 @@ use ArrayAccess; use Illuminate\Contracts\Config\Repository as ConfigContract; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; @@ -177,6 +178,18 @@ public function array(string $key, $default = null): array return $value; } + /** + * Get the specified array configuration value as a collection. + * + * @param string $key + * @param (\Closure():(array|null))|array|null $default + * @return Collection + */ + public function collection(string $key, $default = null): Collection + { + return new Collection($this->array($key, $default)); + } + /** * Set a given configuration value. * diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 07073aab309c..8a5755231b79 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -8,7 +8,9 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\ProcessUtils; +use ReflectionClass; use Symfony\Component\Console\Application as SymfonyApplication; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Input\ArrayInput; @@ -203,6 +205,20 @@ public function output() : ''; } + /** + * Add an array of commands to the console. + * + * @param array $commands + * @return void + */ + #[\Override] + public function addCommands(array $commands): void + { + foreach ($commands as $command) { + $this->add($command); + } + } + /** * Add a command to the console. * @@ -238,12 +254,18 @@ protected function addToParent(SymfonyCommand $command) */ public function resolve($command) { - if (is_subclass_of($command, SymfonyCommand::class) && ($commandName = $command::getDefaultName())) { - foreach (explode('|', $commandName) as $name) { - $this->commandMap[$name] = $command; - } + if (is_subclass_of($command, SymfonyCommand::class)) { + $attribute = (new ReflectionClass($command))->getAttributes(AsCommand::class); - return null; + $commandName = ! empty($attribute) ? $attribute[0]->newInstance()->name : null; + + if (! is_null($commandName)) { + foreach (explode('|', $commandName) as $name) { + $this->commandMap[$name] = $command; + } + + return null; + } } if ($command instanceof Command) { diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index e4be18364599..743a7a9e057e 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -101,9 +101,7 @@ public function __construct() // Once we have constructed the command, we'll set the description and other // related properties of the command. If a signature wasn't used to build // the command we'll set the arguments and the options on this command. - if (! isset($this->description)) { - $this->setDescription((string) static::getDefaultDescription()); - } else { + if (isset($this->description)) { $this->setDescription((string) $this->description); } @@ -263,7 +261,7 @@ protected function resolveCommand($command) * Fail the command manually. * * @param \Throwable|string|null $exception - * @return void + * @return never * * @throws \Illuminate\Console\ManuallyFailedException|\Throwable */ diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index 75cb579925cf..0f0ad16d2c46 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -2,6 +2,7 @@ namespace Illuminate\Console\Scheduling; +use Exception; use Illuminate\Console\Application; use Illuminate\Console\Command; use Illuminate\Console\Events\ScheduledTaskFailed; @@ -21,11 +22,11 @@ class ScheduleRunCommand extends Command { /** - * The console command name. + * The name and signature of the console command. * * @var string */ - protected $name = 'schedule:run'; + protected $signature = 'schedule:run {--whisper : Do not output message indicating that no jobs were ready to run}'; /** * The console command description. @@ -110,8 +111,6 @@ public function handle(Schedule $schedule, Dispatcher $dispatcher, Cache $cache, $this->handler = $handler; $this->phpBinary = Application::phpBinary(); - $this->newLine(); - $events = $this->schedule->dueEvents($this->laravel); if ($events->contains->isRepeatable()) { @@ -125,6 +124,10 @@ public function handle(Schedule $schedule, Dispatcher $dispatcher, Cache $cache, continue; } + if (! $this->eventsRan) { + $this->newLine(); + } + if ($event->onOneServer) { $this->runSingleServerEvent($event); } else { @@ -139,7 +142,9 @@ public function handle(Schedule $schedule, Dispatcher $dispatcher, Cache $cache, } if (! $this->eventsRan) { - $this->components->info('No scheduled commands are ready to run.'); + if (! $this->option('whisper')) { + $this->components->info('No scheduled commands are ready to run.'); + } } else { $this->newLine(); } @@ -197,6 +202,10 @@ protected function runEvent($event) )); $this->eventsRan = true; + + if ($event->exitCode != 0 && ! $event->runInBackground) { + throw new Exception("Scheduled command [{$event->command}] failed with exit code [{$event->exitCode}]."); + } } catch (Throwable $e) { $this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e)); diff --git a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php index 8a1b5c1dec9d..647c4201b2d9 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php @@ -30,7 +30,7 @@ class ScheduleWorkCommand extends Command /** * Execute the console command. * - * @return void + * @return never */ public function handle() { diff --git a/src/Illuminate/Container/Attributes/Context.php b/src/Illuminate/Container/Attributes/Context.php new file mode 100644 index 000000000000..1c858074646d --- /dev/null +++ b/src/Illuminate/Container/Attributes/Context.php @@ -0,0 +1,36 @@ +make(Repository::class); + + return match ($attribute->hidden) { + true => $repository->getHidden($attribute->key, $attribute->default), + false => $repository->get($attribute->key, $attribute->default), + }; + } +} diff --git a/src/Illuminate/Container/Attributes/Give.php b/src/Illuminate/Container/Attributes/Give.php new file mode 100644 index 000000000000..41523a84cc8c --- /dev/null +++ b/src/Illuminate/Container/Attributes/Give.php @@ -0,0 +1,37 @@ + $class + * @param array|null $params + */ + public function __construct( + public string $class, + public array $params = [] + ) { + } + + /** + * Resolve the dependency. + * + * @param self $attribute + * @param \Illuminate\Contracts\Container\Container $container + * @return mixed + */ + public static function resolve(self $attribute, Container $container): mixed + { + return $container->make($attribute->class, $attribute->params); + } +} diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 5c401d465e7f..7e9226e072d5 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -332,7 +332,7 @@ protected function getClosure($abstract, $concrete) } return $container->resolve( - $concrete, $parameters, $raiseEvents = false + $concrete, $parameters, raiseEvents: false ); }; } diff --git a/src/Illuminate/Contracts/Broadcasting/ShouldRescue.php b/src/Illuminate/Contracts/Broadcasting/ShouldRescue.php new file mode 100644 index 000000000000..fad874030a2c --- /dev/null +++ b/src/Illuminate/Contracts/Broadcasting/ShouldRescue.php @@ -0,0 +1,8 @@ +|CastsAttributes|CastsInboundAttributes */ public static function castUsing(array $arguments); diff --git a/src/Illuminate/Contracts/Database/Eloquent/ComparesCastableAttributes.php b/src/Illuminate/Contracts/Database/Eloquent/ComparesCastableAttributes.php new file mode 100644 index 000000000000..5c9ad195b763 --- /dev/null +++ b/src/Illuminate/Contracts/Database/Eloquent/ComparesCastableAttributes.php @@ -0,0 +1,19 @@ +take(1)->get($columns)->first(); + return $this->limit(1)->get($columns)->first(); } /** @@ -395,7 +395,7 @@ public function firstOrFail($columns = ['*'], $message = null) */ public function sole($columns = ['*']) { - $result = $this->take(2)->get($columns); + $result = $this->limit(2)->get($columns); $count = $result->count(); diff --git a/src/Illuminate/Database/Console/DbCommand.php b/src/Illuminate/Database/Console/DbCommand.php index 9737bcab18ea..30176073558d 100644 --- a/src/Illuminate/Database/Console/DbCommand.php +++ b/src/Illuminate/Database/Console/DbCommand.php @@ -157,15 +157,21 @@ public function getCommand(array $connection) */ protected function getMysqlArguments(array $connection) { + $optionalArguments = [ + 'password' => '--password='.$connection['password'], + 'unix_socket' => '--socket='.($connection['unix_socket'] ?? ''), + 'charset' => '--default-character-set='.($connection['charset'] ?? ''), + ]; + + if (! $connection['password']) { + unset($optionalArguments['password']); + } + 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']]); + ], $this->getOptionalArguments($optionalArguments, $connection), [$connection['database']]); } /** diff --git a/src/Illuminate/Database/Console/PruneCommand.php b/src/Illuminate/Database/Console/PruneCommand.php index a7b58e560189..3c1338c8a407 100644 --- a/src/Illuminate/Database/Console/PruneCommand.php +++ b/src/Illuminate/Database/Console/PruneCommand.php @@ -4,9 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\Eloquent\MassPrunable; -use Illuminate\Database\Eloquent\Prunable; -use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Events\ModelPruningFinished; use Illuminate\Database\Events\ModelPruningStarting; use Illuminate\Database\Events\ModelsPruned; @@ -101,7 +99,7 @@ protected function pruneModel(string $model) ? $instance->prunableChunkSize : $this->option('chunk'); - $total = $this->isPrunable($model) + $total = $model::isPrunable() ? $instance->pruneAll($chunkSize) : 0; @@ -117,18 +115,19 @@ protected function pruneModel(string $model) */ protected function models() { - if (! empty($models = $this->option('model'))) { - return (new Collection($models))->filter(function ($model) { - return class_exists($model); - })->values(); - } - + $models = $this->option('model'); $except = $this->option('except'); - if (! empty($models) && ! empty($except)) { + if ($models && $except) { throw new InvalidArgumentException('The --models and --except options cannot be combined.'); } + if ($models) { + return (new Collection($models)) + ->filter(static fn (string $model) => class_exists($model)) + ->values(); + } + return (new Collection(Finder::create()->in($this->getPath())->files()->name('*.php'))) ->map(function ($model) { $namespace = $this->laravel->getNamespace(); @@ -140,7 +139,6 @@ protected function models() ); }) ->when(! empty($except), fn ($models) => $models->reject(fn ($model) => in_array($model, $except))) - ->filter(fn ($model) => class_exists($model)) ->filter(fn ($model) => $this->isPrunable($model)) ->values(); } @@ -161,23 +159,10 @@ protected function getPath() return app_path('Models'); } - /** - * Determine if the given model class is prunable. - * - * @param string $model - * @return bool - */ - protected function isPrunable($model) - { - $uses = class_uses_recursive($model); - - return in_array(Prunable::class, $uses) || in_array(MassPrunable::class, $uses); - } - /** * Display how many models will be pruned. * - * @param string $model + * @param class-string $model * @return void */ protected function pretendToPrune($model) @@ -185,7 +170,7 @@ protected function pretendToPrune($model) $instance = new $model; $count = $instance->prunable() - ->when(in_array(SoftDeletes::class, class_uses_recursive(get_class($instance))), function ($query) { + ->when($model::isSoftDeletable(), function ($query) { $query->withTrashed(); })->count(); @@ -195,4 +180,18 @@ protected function pretendToPrune($model) $this->components->info("{$count} [{$model}] records will be pruned."); } } + + /** + * Determine if the given model is prunable. + * + * @param string $model + * @return bool + */ + private function isPrunable(string $model) + { + return class_exists($model) + && is_a($model, Model::class, true) + && ! (new \ReflectionClass($model))->isAbstract() + && $model::isPrunable(); + } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/UseEloquentBuilder.php b/src/Illuminate/Database/Eloquent/Attributes/UseEloquentBuilder.php new file mode 100644 index 000000000000..c9ac7eb20338 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/UseEloquentBuilder.php @@ -0,0 +1,18 @@ + $builderClass + */ + public function __construct(public string $builderClass) + { + } +} diff --git a/src/Illuminate/Database/Eloquent/Attributes/UsePolicy.php b/src/Illuminate/Database/Eloquent/Attributes/UsePolicy.php new file mode 100644 index 000000000000..9306598e0749 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/UsePolicy.php @@ -0,0 +1,18 @@ + $class + */ + public function __construct(public string $class) + { + } +} diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 4e22b9ae9fa2..2fe6133b49ea 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -118,6 +118,7 @@ class Builder implements BuilderContract 'explain', 'getbindings', 'getconnection', + 'getcountforpagination', 'getgrammar', 'getrawbindings', 'implode', @@ -1118,7 +1119,7 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p // Next we will set the limit and offset for this query so that when we get the // results we get the proper section of results. Then, we'll create the full // paginator instances for these results with the given page and per page. - $this->skip(($page - 1) * $perPage)->take($perPage + 1); + $this->offset(($page - 1) * $perPage)->limit($perPage + 1); return $this->simplePaginator($this->get($columns), $perPage, $page, [ 'path' => Paginator::resolveCurrentPath(), diff --git a/src/Illuminate/Database/Eloquent/Casts/AsFluent.php b/src/Illuminate/Database/Eloquent/Casts/AsFluent.php new file mode 100644 index 000000000000..bba1b1dac9b8 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsFluent.php @@ -0,0 +1,32 @@ + + */ + public static function castUsing(array $arguments) + { + return new class implements CastsAttributes + { + public function get($model, $key, $value, $attributes) + { + return isset($value) ? new Fluent(Json::decode($value)) : null; + } + + public function set($model, $key, $value, $attributes) + { + return isset($value) ? [$key => Json::encode($value)] : null; + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsUri.php b/src/Illuminate/Database/Eloquent/Casts/AsUri.php new file mode 100644 index 000000000000..d55c6d7996b5 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsUri.php @@ -0,0 +1,32 @@ + + */ + public static function castUsing(array $arguments) + { + return new class implements CastsAttributes + { + public function get($model, $key, $value, $attributes) + { + return isset($value) ? new Uri($value) : null; + } + + public function set($model, $key, $value, $attributes) + { + return isset($value) ? (string) $value : null; + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 1c9dad35263f..36d57ffe03c2 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent; +use Closure; use Illuminate\Contracts\Queue\QueueableCollection; use Illuminate\Contracts\Queue\QueueableEntity; use Illuminate\Contracts\Support\Arrayable; @@ -717,8 +718,8 @@ public function partition($key, $operator = null, $value = null) /** * Get an array with the values of a given key. * - * @param string|array|null $value - * @param string|null $key + * @param string|array|Closure|null $value + * @param string|Closure|null $key * @return \Illuminate\Support\Collection */ public function pluck($value, $key = null) diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index 6a02def76ea3..435c4947f769 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -140,8 +140,10 @@ public static function isUnguarded() /** * Run the given callable while being unguarded. * - * @param callable $callback - * @return mixed + * @template TReturn + * + * @param callable(): TReturn $callback + * @return TReturn */ public static function unguarded(callable $callback) { @@ -246,8 +248,8 @@ public function totallyGuarded() /** * Get the fillable attributes of a given array. * - * @param array $attributes - * @return array + * @param array $attributes + * @return array */ protected function fillableFromArray(array $attributes) { diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 0d0fc454bf0b..cce3cac57395 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -50,24 +50,31 @@ trait HasAttributes /** * The model's attributes. * - * @var array + * @var array */ protected $attributes = []; /** * The model attribute's original state. * - * @var array + * @var array */ protected $original = []; /** * The changed model attributes. * - * @var array + * @var array */ protected $changes = []; + /** + * The previous state of the changed model attributes. + * + * @var array + */ + protected $previous = []; + /** * The attributes that should be cast. * @@ -202,7 +209,7 @@ protected function initializeHasAttributes() /** * Convert the model's attributes to an array. * - * @return array + * @return array */ public function attributesToArray() { @@ -237,8 +244,8 @@ public function attributesToArray() /** * Add the date attributes to the attributes array. * - * @param array $attributes - * @return array + * @param array $attributes + * @return array */ protected function addDateAttributesToArray(array $attributes) { @@ -258,9 +265,9 @@ protected function addDateAttributesToArray(array $attributes) /** * Add the mutated attributes to the attributes array. * - * @param array $attributes - * @param array $mutatedAttributes - * @return array + * @param array $attributes + * @param array $mutatedAttributes + * @return array */ protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes) { @@ -286,9 +293,9 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated /** * Add the casted attributes to the attributes array. * - * @param array $attributes - * @param array $mutatedAttributes - * @return array + * @param array $attributes + * @param array $mutatedAttributes + * @return array */ protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) { @@ -341,7 +348,7 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt /** * Get an attribute array of all arrayable attributes. * - * @return array + * @return array */ protected function getArrayableAttributes() { @@ -983,6 +990,21 @@ protected function serializeClassCastableAttribute($key, $value) ); } + /** + * Compare two values for the given attribute using the custom cast class. + * + * @param string $key + * @param mixed $original + * @param mixed $value + * @return bool + */ + protected function compareClassCastableAttribute($key, $original, $value) + { + return $this->resolveCasterClass($key)->compare( + $this, $key, $original, $value + ); + } + /** * Determine if the cast type is a custom date time cast. * @@ -1414,7 +1436,7 @@ public static function encryptUsing($encrypter) * * @return \Illuminate\Contracts\Encryption\Encrypter */ - protected static function currentEncrypter() + public static function currentEncrypter() { return static::$encrypter ?? Crypt::getFacadeRoot(); } @@ -1793,6 +1815,19 @@ protected function isClassSerializable($key) method_exists($this->resolveCasterClass($key), 'serialize'); } + /** + * Determine if the key is comparable using a custom class. + * + * @param string $key + * @return bool + */ + protected function isClassComparable($key) + { + return ! $this->isEnumCastable($key) && + $this->isClassCastable($key) && + method_exists($this->resolveCasterClass($key), 'compare'); + } + /** * Resolve the custom caster class for a given key. * @@ -1953,7 +1988,7 @@ public function setRawAttributes(array $attributes, $sync = false) * * @param string|null $key * @param mixed $default - * @return mixed|array + * @return ($key is null ? array : mixed) */ public function getOriginal($key = null, $default = null) { @@ -1967,7 +2002,7 @@ public function getOriginal($key = null, $default = null) * * @param string|null $key * @param mixed $default - * @return mixed|array + * @return ($key is null ? array : mixed) */ protected function getOriginalWithoutRewindingModel($key = null, $default = null) { @@ -1987,7 +2022,7 @@ protected function getOriginalWithoutRewindingModel($key = null, $default = null * * @param string|null $key * @param mixed $default - * @return mixed|array + * @return ($key is null ? array : mixed) */ public function getRawOriginal($key = null, $default = null) { @@ -1997,8 +2032,8 @@ public function getRawOriginal($key = null, $default = null) /** * Get a subset of the model's attributes. * - * @param array|mixed $attributes - * @return array + * @param array|mixed $attributes + * @return array */ public function only($attributes) { @@ -2014,7 +2049,7 @@ public function only($attributes) /** * Get all attributes except the given ones. * - * @param array|mixed $attributes + * @param array|mixed $attributes * @return array */ public function except($attributes) @@ -2058,7 +2093,7 @@ public function syncOriginalAttribute($attribute) /** * Sync multiple original attribute with their current values. * - * @param array|string $attributes + * @param array|string $attributes * @return $this */ public function syncOriginalAttributes($attributes) @@ -2082,6 +2117,7 @@ public function syncOriginalAttributes($attributes) public function syncChanges() { $this->changes = $this->getDirty(); + $this->previous = array_intersect_key($this->getRawOriginal(), $this->changes); return $this; } @@ -2089,7 +2125,7 @@ public function syncChanges() /** * Determine if the model or any of the given attribute(s) have been modified. * - * @param array|string|null $attributes + * @param array|string|null $attributes * @return bool */ public function isDirty($attributes = null) @@ -2102,7 +2138,7 @@ public function isDirty($attributes = null) /** * Determine if the model or all the given attribute(s) have remained the same. * - * @param array|string|null $attributes + * @param array|string|null $attributes * @return bool */ public function isClean($attributes = null) @@ -2117,7 +2153,10 @@ public function isClean($attributes = null) */ public function discardChanges() { - [$this->attributes, $this->changes] = [$this->original, []]; + [$this->attributes, $this->changes, $this->previous] = [$this->original, [], []]; + + $this->classCastCache = []; + $this->attributeCastCache = []; return $this; } @@ -2125,7 +2164,7 @@ public function discardChanges() /** * Determine if the model or any of the given attribute(s) were changed when the model was last saved. * - * @param array|string|null $attributes + * @param array|string|null $attributes * @return bool */ public function wasChanged($attributes = null) @@ -2138,8 +2177,8 @@ public function wasChanged($attributes = null) /** * Determine if any of the given attributes were changed when the model was last saved. * - * @param array $changes - * @param array|string|null $attributes + * @param array $changes + * @param array|string|null $attributes * @return bool */ protected function hasChanges($changes, $attributes = null) @@ -2166,7 +2205,7 @@ protected function hasChanges($changes, $attributes = null) /** * Get the attributes that have been changed since the last sync. * - * @return array + * @return array */ public function getDirty() { @@ -2184,7 +2223,7 @@ public function getDirty() /** * Get the attributes that have been changed since the last sync for an update operation. * - * @return array + * @return array */ protected function getDirtyForUpdate() { @@ -2194,13 +2233,23 @@ protected function getDirtyForUpdate() /** * Get the attributes that were changed when the model was last saved. * - * @return array + * @return array */ public function getChanges() { return $this->changes; } + /** + * Get the attributes that were previously original before the model was last saved. + * + * @return array + */ + public function getPrevious() + { + return $this->previous; + } + /** * Determine if the new and old values for a given key are equivalent. * @@ -2247,6 +2296,8 @@ public function originalIsEquivalent($key) } return false; + } elseif ($this->isClassComparable($key)) { + return $this->compareClassCastableAttribute($key, $original, $attribute); } return is_numeric($attribute) && is_numeric($original) @@ -2299,7 +2350,7 @@ protected function transformModelValue($key, $value) /** * Append attributes to query when building a query. * - * @param array|string $attributes + * @param array|string $attributes * @return $this */ public function append($attributes) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index a52d840f421e..30607e7c45fc 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; @@ -518,7 +517,7 @@ protected function expandAttributes(array $definition) /** * Add a new state transformation to the model definition. * - * @param (callable(array, TModel|null): array)|array $state + * @param (callable(array, Model|null): array)|array $state * @return static */ public function state($state) @@ -533,7 +532,7 @@ public function state($state) /** * Prepend a new state transformation to the model definition. * - * @param (callable(array, TModel|null): array)|array $state + * @param (callable(array, Model|null): array)|array $state * @return static */ public function prependState($state) @@ -971,7 +970,7 @@ public function __call($method, $parameters) return $this->macroCall($method, $parameters); } - if ($method === 'trashed' && in_array(SoftDeletes::class, class_uses_recursive($this->modelName()))) { + if ($method === 'trashed' && $this->modelName()::isSoftDeletable()) { return $this->state([ $this->newModel()->getDeletedAtColumn() => $parameters[0] ?? Carbon::now()->subDay(), ]); diff --git a/src/Illuminate/Database/Eloquent/MassPrunable.php b/src/Illuminate/Database/Eloquent/MassPrunable.php index 81e2701263ca..6111ffd86b85 100644 --- a/src/Illuminate/Database/Eloquent/MassPrunable.php +++ b/src/Illuminate/Database/Eloquent/MassPrunable.php @@ -23,7 +23,7 @@ public function pruneAll(int $chunkSize = 1000) $total = 0; - $softDeletable = in_array(SoftDeletes::class, class_uses_recursive(get_class($this))); + $softDeletable = static::isSoftDeletable(); do { $total += $count = $softDeletable diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 72d7e3315e36..d932bd38e8ed 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -13,6 +13,7 @@ use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Eloquent\Attributes\Scope as LocalScope; +use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; @@ -26,6 +27,7 @@ use JsonException; use JsonSerializable; use LogicException; +use ReflectionClass; use ReflectionMethod; use Stringable; @@ -248,6 +250,27 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static string $collectionClass = Collection::class; + /** + * Cache of soft deletable models. + * + * @var array, bool> + */ + protected static array $isSoftDeletable; + + /** + * Cache of prunable models. + * + * @var array, bool> + */ + protected static array $isPrunable; + + /** + * Cache of mass prunable models. + * + * @var array, bool> + */ + protected static array $isMassPrunable; + /** * The name of the "created at" column. * @@ -265,7 +288,7 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt /** * Create a new Eloquent model instance. * - * @param array $attributes + * @param array $attributes */ public function __construct(array $attributes = []) { @@ -568,7 +591,7 @@ public static function withoutBroadcasting(callable $callback) /** * Fill the model with an array of attributes. * - * @param array $attributes + * @param array $attributes * @return $this * * @throws \Illuminate\Database\Eloquent\MassAssignmentException @@ -618,7 +641,7 @@ public function fill(array $attributes) /** * Fill the model with an array of attributes. Force mass assignment. * - * @param array $attributes + * @param array $attributes * @return $this */ public function forceFill(array $attributes) @@ -657,7 +680,7 @@ public function qualifyColumns($columns) /** * Create a new instance of the given model. * - * @param array $attributes + * @param array $attributes * @param bool $exists * @return static */ @@ -686,7 +709,7 @@ public function newInstance($attributes = [], $exists = false) /** * Create a new model instance that is existing. * - * @param array $attributes + * @param array $attributes * @param string|null $connection * @return static */ @@ -1049,8 +1072,8 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) /** * Update the model in the database. * - * @param array $attributes - * @param array $options + * @param array $attributes + * @param array $options * @return bool */ public function update(array $attributes = [], array $options = []) @@ -1065,8 +1088,8 @@ public function update(array $attributes = [], array $options = []) /** * Update the model in the database within a transaction. * - * @param array $attributes - * @param array $options + * @param array $attributes + * @param array $options * @return bool * * @throws \Throwable @@ -1083,8 +1106,8 @@ public function updateOrFail(array $attributes = [], array $options = []) /** * Update the model in the database without raising any events. * - * @param array $attributes - * @param array $options + * @param array $attributes + * @param array $options * @return bool */ public function updateQuietly(array $attributes = [], array $options = []) @@ -1400,7 +1423,7 @@ protected function performInsert(Builder $query) * Insert the given attributes and set the ID on the model. * * @param \Illuminate\Database\Eloquent\Builder $query - * @param array $attributes + * @param array $attributes * @return void */ protected function insertAndSetId(Builder $query, $attributes) @@ -1651,9 +1674,30 @@ public function newQueryForRestoration($ids) */ public function newEloquentBuilder($query) { + $builderClass = $this->resolveCustomBuilderClass(); + + if ($builderClass && is_subclass_of($builderClass, Builder::class)) { + return new $builderClass($query); + } + return new static::$builder($query); } + /** + * Resolve the custom Eloquent builder class from the model attributes. + * + * @return class-string<\Illuminate\Database\Eloquent\Builder>|false + */ + protected function resolveCustomBuilderClass() + { + $attributes = (new ReflectionClass($this)) + ->getAttributes(UseEloquentBuilder::class); + + return ! empty($attributes) + ? $attributes[0]->newInstance()->builderClass + : false; + } + /** * Get a new query builder instance for the connection. * @@ -1668,7 +1712,7 @@ protected function newBaseQueryBuilder() * Create a new pivot model instance. * * @param \Illuminate\Database\Eloquent\Model $parent - * @param array $attributes + * @param array $attributes * @param string $table * @param bool $exists * @param string|null $using @@ -2266,6 +2310,30 @@ public function setPerPage($perPage) return $this; } + /** + * Determine if the model is soft deletable. + */ + public static function isSoftDeletable(): bool + { + return static::$isSoftDeletable[static::class] ??= in_array(SoftDeletes::class, class_uses_recursive(static::class)); + } + + /** + * Determine if the model is prunable. + */ + protected function isPrunable(): bool + { + return self::$isPrunable[static::class] ??= in_array(Prunable::class, class_uses_recursive(static::class)) || static::isMassPrunable(); + } + + /** + * Determine if the model is mass prunable. + */ + protected function isMassPrunable(): bool + { + return self::$isMassPrunable[static::class] ??= in_array(MassPrunable::class, class_uses_recursive(static::class)); + } + /** * Determine if lazy loading is disabled. * diff --git a/src/Illuminate/Database/Eloquent/Prunable.php b/src/Illuminate/Database/Eloquent/Prunable.php index b1314af362e5..1eba87174804 100644 --- a/src/Illuminate/Database/Eloquent/Prunable.php +++ b/src/Illuminate/Database/Eloquent/Prunable.php @@ -20,7 +20,7 @@ public function pruneAll(int $chunkSize = 1000) $total = 0; $this->prunable() - ->when(in_array(SoftDeletes::class, class_uses_recursive(static::class)), function ($query) { + ->when(static::isSoftDeletable(), function ($query) { $query->withTrashed(); })->chunkById($chunkSize, function ($models) use (&$total) { $models->each(function ($model) use (&$total) { @@ -64,7 +64,7 @@ public function prune() { $this->pruning(); - return in_array(SoftDeletes::class, class_uses_recursive(static::class)) + return static::isSoftDeletable() ? $this->forceDelete() : $this->delete(); } diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index e125a760410b..c06da80ce42a 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -841,7 +841,7 @@ public function firstWhere($column, $operator = null, $value = null, $boolean = */ public function first($columns = ['*']) { - $results = $this->take(1)->get($columns); + $results = $this->limit(1)->get($columns); return count($results) > 0 ? $results->first() : null; } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 8de013a1a38a..15e60760f235 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -66,7 +66,7 @@ public function toggle($ids, $touch = true) /** * Sync the intermediate tables with a list of IDs without detaching. * - * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids + * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids * @return array{attached: array, detached: array, updated: array} */ public function syncWithoutDetaching($ids) @@ -77,7 +77,7 @@ public function syncWithoutDetaching($ids) /** * Sync the intermediate tables with a list of IDs or collection of models. * - * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids + * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids * @param bool $detaching * @return array{attached: array, detached: array, updated: array} */ @@ -87,14 +87,18 @@ public function sync($ids, $detaching = true) 'attached' => [], 'detached' => [], 'updated' => [], ]; + $records = $this->formatRecordsList($this->parseIds($ids)); + + if (empty($records) && ! $detaching) { + return $changes; + } + // First we need to attach any of the associated models that are not currently // in this joining table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. $current = $this->getCurrentlyAttachedPivots() ->pluck($this->relatedPivotKey)->all(); - $records = $this->formatRecordsList($this->parseIds($ids)); - // Next, we will take the differences of the currents and given IDs and detach // all of the entities that exist in the "current" array but are not in the // array of the new IDs given to the method which will complete the sync. @@ -130,7 +134,7 @@ public function sync($ids, $detaching = true) /** * 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 \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array|int|string $ids * @param array $values * @param bool $detaching * @return array{attached: array, detached: array, updated: array} diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php index 97c011d6cefb..27a944201f4e 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; -use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Grammars\MySqlGrammar; use Illuminate\Database\UniqueConstraintViolationException; @@ -146,7 +145,7 @@ public function getQualifiedParentKeyName() */ public function throughParentSoftDeletes() { - return in_array(SoftDeletes::class, class_uses_recursive($this->throughParent)); + return $this->throughParent::isSoftDeletable(); } /** @@ -280,7 +279,7 @@ public function firstWhere($column, $operator = null, $value = null, $boolean = */ public function first($columns = ['*']) { - $results = $this->take(1)->get($columns); + $results = $this->limit(1)->get($columns); return count($results) > 0 ? $results->first() : null; } diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index ad7d75168e78..3f20b1d74b93 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -186,7 +186,7 @@ public function getEager() */ public function sole($columns = ['*']) { - $result = $this->take(2)->get($columns); + $result = $this->limit(2)->get($columns); $count = $result->count(); diff --git a/src/Illuminate/Database/Eloquent/Scope.php b/src/Illuminate/Database/Eloquent/Scope.php index 63cba6a51717..cfb1d9b97bc1 100644 --- a/src/Illuminate/Database/Eloquent/Scope.php +++ b/src/Illuminate/Database/Eloquent/Scope.php @@ -7,8 +7,10 @@ interface Scope /** * Apply the scope to a given Eloquent query builder. * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @param \Illuminate\Database\Eloquent\Model $model + * @template TModel of \Illuminate\Database\Eloquent\Model + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param TModel $model * @return void */ public function apply(Builder $builder, Model $model); diff --git a/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php b/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php index a762da81b603..8f093b4666a5 100755 --- a/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php +++ b/src/Illuminate/Database/Migrations/DatabaseMigrationRepository.php @@ -64,7 +64,9 @@ public function getMigrations($steps) return $query->orderBy('batch', 'desc') ->orderBy('migration', 'desc') - ->take($steps)->get()->all(); + ->limit($steps) + ->get() + ->all(); } /** diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index d2b97d5d121e..1a97a0799c33 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -2856,6 +2856,17 @@ public function reorder($column = null, $direction = 'asc') return $this; } + /** + * Add descending "reorder" clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string|null $column + * @return $this + */ + public function reorderDesc($column) + { + return $this->reorder($column, 'desc'); + } + /** * Get an array with all orders with a given column removed. * @@ -3251,7 +3262,7 @@ protected function ensureOrderForCursorPagination($shouldReverse = false) * Get the count of the total records for the paginator. * * @param array $columns - * @return int + * @return int<0, max> */ public function getCountForPagination($columns = ['*']) { @@ -3540,7 +3551,7 @@ public function doesntExistOr(Closure $callback) * Retrieve the "count" result of the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $columns - * @return int + * @return int<0, max> */ public function count($columns = '*') { @@ -3742,7 +3753,7 @@ public function insert(array $values) /** * Insert new records into the database while ignoring errors. * - * @return int + * @return int<0, max> */ public function insertOrIgnore(array $values) { @@ -3824,7 +3835,7 @@ public function insertOrIgnoreUsing(array $columns, $query) /** * Update records in the database. * - * @return int + * @return int<0, max> */ public function update(array $values) { @@ -3898,11 +3909,9 @@ public function updateOrInsert(array $attributes, array|callable $values = []) /** * Insert new records or update the existing ones. * - * @param array|string $uniqueBy - * @param array|null $update * @return int */ - public function upsert(array $values, $uniqueBy, $update = null) + public function upsert(array $values, array|string $uniqueBy, ?array $update = null) { if (empty($values)) { return 0; @@ -3944,7 +3953,7 @@ public function upsert(array $values, $uniqueBy, $update = null) * * @param string $column * @param float|int $amount - * @return int + * @return int<0, max> * * @throws \InvalidArgumentException */ @@ -3962,7 +3971,7 @@ public function increment($column, $amount = 1, array $extra = []) * * @param array $columns * @param array $extra - * @return int + * @return int<0, max> * * @throws \InvalidArgumentException */ @@ -3986,7 +3995,7 @@ public function incrementEach(array $columns, array $extra = []) * * @param string $column * @param float|int $amount - * @return int + * @return int<0, max> * * @throws \InvalidArgumentException */ @@ -4004,7 +4013,7 @@ public function decrement($column, $amount = 1, array $extra = []) * * @param array $columns * @param array $extra - * @return int + * @return int<0, max> * * @throws \InvalidArgumentException */ diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index b7687e839f34..07cb721eefbc 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -1239,13 +1239,14 @@ public function timestampTz($column, $precision = null) * Add nullable creation and update timestamps to the table. * * @param int|null $precision - * @return void + * @return \Illuminate\Support\Collection */ public function timestamps($precision = null) { - $this->timestamp('created_at', $precision)->nullable(); - - $this->timestamp('updated_at', $precision)->nullable(); + return new Collection([ + $this->timestamp('created_at', $precision)->nullable(), + $this->timestamp('updated_at', $precision)->nullable(), + ]); } /** @@ -1254,37 +1255,39 @@ public function timestamps($precision = null) * Alias for self::timestamps(). * * @param int|null $precision - * @return void + * @return \Illuminate\Support\Collection */ public function nullableTimestamps($precision = null) { - $this->timestamps($precision); + return $this->timestamps($precision); } /** * Add creation and update timestampTz columns to the table. * * @param int|null $precision - * @return void + * @return \Illuminate\Support\Collection */ public function timestampsTz($precision = null) { - $this->timestampTz('created_at', $precision)->nullable(); - - $this->timestampTz('updated_at', $precision)->nullable(); + return new Collection([ + $this->timestampTz('created_at', $precision)->nullable(), + $this->timestampTz('updated_at', $precision)->nullable(), + ]); } /** * Add creation and update datetime columns to the table. * * @param int|null $precision - * @return void + * @return \Illuminate\Support\Collection */ public function datetimes($precision = null) { - $this->datetime('created_at', $precision)->nullable(); - - $this->datetime('updated_at', $precision)->nullable(); + return new Collection([ + $this->datetime('created_at', $precision)->nullable(), + $this->datetime('updated_at', $precision)->nullable(), + ]); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index ed683d256d30..9e17e6204a36 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -478,12 +478,12 @@ protected function getDefaultValue($value) } if ($value instanceof BackedEnum) { - return "'{$value->value}'"; + return "'".str_replace("'", "''", $value->value)."'"; } return is_bool($value) ? "'".(int) $value."'" - : "'".(string) $value."'"; + : "'".str_replace("'", "''", $value)."'"; } /** diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 1635de7742e5..427c943ff736 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -115,10 +115,10 @@ protected function connectionString() $value .= ' --ssl-ca="${:LARAVEL_LOAD_SSL_CA}"'; } - if (isset($config['options'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT]) && - $config['options'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] === false) { - $value .= ' --ssl=off'; - } + // if (isset($config['options'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT]) && + // $config['options'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] === false) { + // $value .= ' --ssl=off'; + // } return $value; } diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index 606c093f1ba9..dcf37d499b52 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -17,7 +17,7 @@ "require": { "php": "^8.2", "ext-pdo": "*", - "brick/math": "^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13", "illuminate/collections": "^12.0", "illuminate/container": "^12.0", "illuminate/contracts": "^12.0", diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index b9faeb152595..e8e39b6fa982 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '12.14.0'; + const VERSION = '12.19.3'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Console/AboutCommand.php b/src/Illuminate/Foundation/Console/AboutCommand.php index c6d61b6d8303..8df4e28e1861 100644 --- a/src/Illuminate/Foundation/Console/AboutCommand.php +++ b/src/Illuminate/Foundation/Console/AboutCommand.php @@ -183,7 +183,7 @@ protected function gatherApplicationInformation() 'Config' => static::format($this->laravel->configurationIsCached(), console: $formatCachedStatus), 'Events' => static::format($this->laravel->eventsAreCached(), console: $formatCachedStatus), 'Routes' => static::format($this->laravel->routesAreCached(), console: $formatCachedStatus), - 'Views' => static::format($this->hasPhpFiles($this->laravel->storagePath('framework/views')), console: $formatCachedStatus), + 'Views' => static::format($this->hasPhpFiles(config('view.compiled')), console: $formatCachedStatus), ]); static::addToSection('Drivers', fn () => array_filter([ diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index b23689671d18..7ee4c1f81313 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -5,12 +5,16 @@ use Composer\InstalledVersions; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Env; use Illuminate\Support\Facades\Process; use Symfony\Component\Console\Attribute\AsCommand; use function Illuminate\Support\artisan_binary; use function Illuminate\Support\php_binary; use function Laravel\Prompts\confirm; +use function Laravel\Prompts\password; +use function Laravel\Prompts\select; +use function Laravel\Prompts\text; #[AsCommand(name: 'install:broadcasting')] class BroadcastingInstallCommand extends Command @@ -26,6 +30,9 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} + {--reverb : Install Laravel Reverb as the default broadcaster} + {--pusher : Install Pusher as the default broadcaster} + {--ably : Install Ably as the default broadcaster} {--without-node : Do not prompt to install Node dependencies}'; /** @@ -35,10 +42,27 @@ class BroadcastingInstallCommand extends Command */ protected $description = 'Create a broadcasting channel routes file'; + /** + * The broadcasting driver to use. + * + * @var string|null + */ + protected $driver = null; + + /** + * The framework packages to install. + * + * @var array + */ + protected $frameworkPackages = [ + 'react' => '@laravel/echo-react', + 'vue' => '@laravel/echo-vue', + ]; + /** * Execute the console command. * - * @return int + * @return void */ public function handle() { @@ -54,25 +78,56 @@ public function handle() $this->uncommentChannelsRoutesFile(); $this->enableBroadcastServiceProvider(); - // Install bootstrapping... - if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { - if (! is_dir($directory = $this->laravel->resourcePath('js'))) { - mkdir($directory, 0755, true); - } + $this->driver = $this->resolveDriver(); - copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); - } + Env::writeVariable('BROADCAST_CONNECTION', $this->driver, $this->laravel->basePath('.env'), true); - if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { - $bootstrapScript = file_get_contents( - $bootstrapScriptPath - ); + $this->collectDriverConfig(); + $this->installDriverPackages(); + + if ($this->isUsingSupportedFramework()) { + // If this is a supported framework, we will use the framework-specific Echo helpers... + $this->injectFrameworkSpecificConfiguration(); + } else { + // Standard JavaScript implementation... + if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { + if (! is_dir($directory = $this->laravel->resourcePath('js'))) { + mkdir($directory, 0755, true); + } + + $stubPath = __DIR__.'/stubs/echo-js-'.$this->driver.'.stub'; + + if (! file_exists($stubPath)) { + $stubPath = __DIR__.'/stubs/echo-js-reverb.stub'; + } + + copy($stubPath, $echoScriptPath); + } + + // Only add the bootstrap import for the standard JS implementation... + if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { + $bootstrapScript = file_get_contents( + $bootstrapScriptPath + ); - if (! str_contains($bootstrapScript, './echo')) { - file_put_contents( - $bootstrapScriptPath, - trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + if (! str_contains($bootstrapScript, './echo')) { + file_put_contents( + $bootstrapScriptPath, + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } + } elseif (file_exists($appScriptPath = $this->laravel->resourcePath('js/app.js'))) { + // If no bootstrap.js, try app.js... + $appScript = file_get_contents( + $appScriptPath ); + + if (! str_contains($appScript, './echo')) { + file_put_contents( + $appScriptPath, + trim($appScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } } } @@ -118,8 +173,10 @@ protected function enableBroadcastServiceProvider() { $filesystem = new Filesystem; - if (! $filesystem->exists(app()->configPath('app.php')) || - ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php')) { + if ( + ! $filesystem->exists(app()->configPath('app.php')) || + ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php') + ) { return; } @@ -134,6 +191,171 @@ protected function enableBroadcastServiceProvider() } } + /** + * Collect the driver configuration. + * + * @return void + */ + protected function collectDriverConfig() + { + $envPath = $this->laravel->basePath('.env'); + + if (! file_exists($envPath)) { + return; + } + + match ($this->driver) { + 'pusher' => $this->collectPusherConfig(), + 'ably' => $this->collectAblyConfig(), + default => null, + }; + } + + /** + * Install the driver packages. + * + * @return void + */ + protected function installDriverPackages() + { + $package = match ($this->driver) { + 'pusher' => 'pusher/pusher-php-server', + 'ably' => 'ably/ably-php', + default => null, + }; + + if (! $package || InstalledVersions::isInstalled($package)) { + return; + } + + $this->requireComposerPackages($this->option('composer'), [$package]); + } + + /** + * Collect the Pusher configuration. + * + * @return void + */ + protected function collectPusherConfig() + { + $appId = text('Pusher App ID', 'Enter your Pusher app ID'); + $key = password('Pusher App Key', 'Enter your Pusher app key'); + $secret = password('Pusher App Secret', 'Enter your Pusher app secret'); + + $cluster = select('Pusher App Cluster', [ + 'mt1', + 'us2', + 'us3', + 'eu', + 'ap1', + 'ap2', + 'ap3', + 'ap4', + 'sa1', + ]); + + Env::writeVariables([ + 'PUSHER_APP_ID' => $appId, + 'PUSHER_APP_KEY' => $key, + 'PUSHER_APP_SECRET' => $secret, + 'PUSHER_APP_CLUSTER' => $cluster, + 'PUSHER_PORT' => 443, + 'PUSHER_SCHEME' => 'https', + 'VITE_PUSHER_APP_KEY' => '${PUSHER_APP_KEY}', + 'VITE_PUSHER_APP_CLUSTER' => '${PUSHER_APP_CLUSTER}', + 'VITE_PUSHER_HOST' => '${PUSHER_HOST}', + 'VITE_PUSHER_PORT' => '${PUSHER_PORT}', + 'VITE_PUSHER_SCHEME' => '${PUSHER_SCHEME}', + ], $this->laravel->basePath('.env')); + } + + /** + * Collect the Ably configuration. + * + * @return void + */ + protected function collectAblyConfig() + { + $this->components->warn('Make sure to enable "Pusher protocol support" in your Ably app settings.'); + + $key = password('Ably Key', 'Enter your Ably key'); + + $publicKey = explode(':', $key)[0] ?? $key; + + Env::writeVariables([ + 'ABLY_KEY' => $key, + 'ABLY_PUBLIC_KEY' => $publicKey, + 'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}', + ], $this->laravel->basePath('.env')); + } + + /** + * Inject Echo configuration into the application's main file. + * + * @return void + */ + protected function injectFrameworkSpecificConfiguration() + { + if ($this->appUsesVue()) { + $importPath = $this->frameworkPackages['vue']; + + $filePaths = [ + $this->laravel->resourcePath('js/app.ts'), + $this->laravel->resourcePath('js/app.js'), + ]; + } else { + $importPath = $this->frameworkPackages['react']; + + $filePaths = [ + $this->laravel->resourcePath('js/app.tsx'), + $this->laravel->resourcePath('js/app.jsx'), + ]; + } + + $filePath = array_filter($filePaths, function ($path) { + return file_exists($path); + })[0] ?? null; + + if (! $filePath) { + $this->components->warn("Could not find file [{$filePaths[0]}]. Skipping automatic Echo configuration."); + + return; + } + + $contents = file_get_contents($filePath); + + $echoCode = <<driver}', + }); + JS; + + preg_match_all('/^import .+;$/m', $contents, $matches); + + if (empty($matches[0])) { + // Add the Echo configuration to the top of the file if no import statements are found... + $newContents = $echoCode.PHP_EOL.$contents; + + file_put_contents($filePath, $newContents); + } else { + // Add Echo configuration after the last import... + $lastImport = end($matches[0]); + + $positionOfLastImport = strrpos($contents, $lastImport); + + if ($positionOfLastImport !== false) { + $insertPosition = $positionOfLastImport + strlen($lastImport); + $newContents = substr($contents, 0, $insertPosition).PHP_EOL.$echoCode.substr($contents, $insertPosition); + + file_put_contents($filePath, $newContents); + } + } + + $this->components->info('Echo configuration added to ['.basename($filePath).'].'); + } + /** * Install Laravel Reverb into the application if desired. * @@ -141,7 +363,7 @@ protected function enableBroadcastServiceProvider() */ protected function installReverb() { - if ($this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { + if ($this->driver !== 'reverb' || $this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { return; } @@ -199,6 +421,12 @@ protected function installNodeDependencies() ]; } + if ($this->appUsesVue()) { + $commands[0] .= ' '.$this->frameworkPackages['vue']; + } elseif ($this->appUsesReact()) { + $commands[0] .= ' '.$this->frameworkPackages['react']; + } + $command = Process::command(implode(' && ', $commands)) ->path(base_path()); @@ -212,4 +440,79 @@ protected function installNodeDependencies() $this->components->info('Node dependencies installed successfully.'); } } + + /** + * Resolve the provider to use based on the user's choice. + * + * @return string + */ + protected function resolveDriver(): string + { + if ($this->option('reverb')) { + return 'reverb'; + } + + if ($this->option('pusher')) { + return 'pusher'; + } + + if ($this->option('ably')) { + return 'ably'; + } + + return select('Which broadcasting driver would you like to use?', [ + 'reverb' => 'Laravel Reverb', + 'pusher' => 'Pusher', + 'ably' => 'Ably', + ]); + } + + /** + * Detect if the user is using a supported framework (React or Vue). + * + * @return bool + */ + protected function isUsingSupportedFramework(): bool + { + return $this->appUsesReact() || $this->appUsesVue(); + } + + /** + * Detect if the user is using React. + * + * @return bool + */ + protected function appUsesReact(): bool + { + return $this->packageDependenciesInclude('react'); + } + + /** + * Detect if the user is using Vue. + * + * @return bool + */ + protected function appUsesVue(): bool + { + return $this->packageDependenciesInclude('vue'); + } + + /** + * Detect if the package is installed. + * + * @return bool + */ + protected function packageDependenciesInclude(string $package): bool + { + $packageJsonPath = $this->laravel->basePath('package.json'); + + if (! file_exists($packageJsonPath)) { + return false; + } + + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + + return isset($packageJson['dependencies'][$package]) || + isset($packageJson['devDependencies'][$package]); + } } diff --git a/src/Illuminate/Foundation/Console/ClosureCommand.php b/src/Illuminate/Foundation/Console/ClosureCommand.php index a9817d4df22d..f781ba44d2ea 100644 --- a/src/Illuminate/Foundation/Console/ClosureCommand.php +++ b/src/Illuminate/Foundation/Console/ClosureCommand.php @@ -25,6 +25,13 @@ class ClosureCommand extends Command */ protected $callback; + /** + * The console command description. + * + * @var string + */ + protected $description = ''; + /** * Create a new command instance. * diff --git a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php index 221ef95caecb..a105ceaee205 100644 --- a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php @@ -48,7 +48,7 @@ public function handle() } if (parent::handle() === false && ! $this->option('force')) { - return false; + return; } if (! $this->option('inline')) { diff --git a/src/Illuminate/Foundation/Console/EventCacheCommand.php b/src/Illuminate/Foundation/Console/EventCacheCommand.php index 9039d4c20a22..4b6edf67d8ad 100644 --- a/src/Illuminate/Foundation/Console/EventCacheCommand.php +++ b/src/Illuminate/Foundation/Console/EventCacheCommand.php @@ -26,7 +26,7 @@ class EventCacheCommand extends Command /** * Execute the console command. * - * @return mixed + * @return void */ public function handle() { diff --git a/src/Illuminate/Foundation/Console/JobMakeCommand.php b/src/Illuminate/Foundation/Console/JobMakeCommand.php index 9f0f1b0e9ffc..43d2f161a749 100644 --- a/src/Illuminate/Foundation/Console/JobMakeCommand.php +++ b/src/Illuminate/Foundation/Console/JobMakeCommand.php @@ -40,6 +40,10 @@ class JobMakeCommand extends GeneratorCommand */ protected function getStub() { + if ($this->option('batched')) { + return $this->resolveStubPath('/stubs/job.batched.queued.stub'); + } + return $this->option('sync') ? $this->resolveStubPath('/stubs/job.stub') : $this->resolveStubPath('/stubs/job.queued.stub'); @@ -78,7 +82,8 @@ protected function getOptions() { return [ ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the job already exists'], - ['sync', null, InputOption::VALUE_NONE, 'Indicates that job should be synchronous'], + ['sync', null, InputOption::VALUE_NONE, 'Indicates that the job should be synchronous'], + ['batched', null, InputOption::VALUE_NONE, 'Indicates that the job should be batchable'], ]; } } diff --git a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php index 974af84f947c..414a0d57dac1 100644 --- a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php @@ -59,9 +59,9 @@ public function handle() public function getOptimizeClearTasks() { return [ + 'config' => 'config:clear', 'cache' => 'cache:clear', 'compiled' => 'clear-compiled', - 'config' => 'config:clear', 'events' => 'event:clear', 'routes' => 'route:clear', 'views' => 'view:clear', diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub new file mode 100644 index 000000000000..ec518d214668 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub @@ -0,0 +1,13 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "pusher", + key: import.meta.env.VITE_ABLY_PUBLIC_KEY, + wsHost: "realtime-pusher.ably.io", + wsPort: 443, + disableStats: true, + encrypted: true, +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub new file mode 100644 index 000000000000..5a8a7f7e31ef --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub @@ -0,0 +1,15 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "pusher", + key: import.meta.env.VITE_PUSHER_APP_KEY, + cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER, + forceTLS: true, + wsHost: import.meta.env.VITE_PUSHER_HOST, + wsPort: import.meta.env.VITE_PUSHER_PORT, + wssPort: import.meta.env.VITE_PUSHER_PORT, + enabledTransports: ["ws", "wss"], +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-reverb.stub similarity index 100% rename from src/Illuminate/Foundation/Console/stubs/echo-js.stub rename to src/Illuminate/Foundation/Console/stubs/echo-js-reverb.stub diff --git a/src/Illuminate/Foundation/Console/stubs/job.batched.queued.stub b/src/Illuminate/Foundation/Console/stubs/job.batched.queued.stub new file mode 100644 index 000000000000..d6888e9add5a --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/job.batched.queued.stub @@ -0,0 +1,34 @@ +batch()->cancelled()) { + // The batch has been cancelled... + + return; + } + + // + } +} diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 89a0c3ac9797..3b1553071978 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -4,7 +4,6 @@ use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; @@ -223,8 +222,7 @@ public function expectsDatabaseQueryCount($expected, $connection = null) */ protected function isSoftDeletableModel($model) { - return $model instanceof Model - && in_array(SoftDeletes::class, class_uses_recursive($model)); + return $model instanceof Model && $model::isSoftDeletable(); } /** diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 4be085daa39c..c372d55cc93f 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -23,9 +23,11 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; +use Illuminate\Mail\Markdown; use Illuminate\Queue\Console\WorkCommand; use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Once; @@ -171,8 +173,10 @@ protected function tearDownTheTestEnvironment(): void Component::forgetFactory(); ConvertEmptyStringsToNull::flushState(); Factory::flushState(); + EncodedHtmlString::flushState(); EncryptCookies::flushState(); HandleExceptions::flushState(); + Markdown::flushState(); Migrator::withoutMigrations([]); Once::flush(); PreventRequestsDuringMaintenance::flushState(); diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index 8f0523dce09f..2580b987cb6c 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -1,5 +1,6 @@ |null $abstract - * @param array $parameters * @return ($abstract is class-string ? TClass : ($abstract is null ? \Illuminate\Foundation\Application : mixed)) */ function app($abstract = null, array $parameters = []) @@ -227,6 +224,42 @@ function broadcast($event = null) } } +if (! function_exists('broadcast_if')) { + /** + * Begin broadcasting an event if the given condition is true. + * + * @param bool $boolean + * @param mixed|null $event + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + function broadcast_if($boolean, $event = null) + { + if ($boolean) { + return app(BroadcastFactory::class)->event($event); + } else { + return new FakePendingBroadcast; + } + } +} + +if (! function_exists('broadcast_unless')) { + /** + * Begin broadcasting an event unless the given condition is true. + * + * @param bool $boolean + * @param mixed|null $event + * @return \Illuminate\Broadcasting\PendingBroadcast + */ + function broadcast_unless($boolean, $event = null) + { + if (! $boolean) { + return app(BroadcastFactory::class)->event($event); + } else { + return new FakePendingBroadcast; + } + } +} + if (! function_exists('cache')) { /** * Get / set the specified cache value. @@ -406,9 +439,6 @@ function decrypt($value, $unserialize = true) /** * Defer execution of the given callback. * - * @param callable|null $callback - * @param string|null $name - * @param bool $always * @return \Illuminate\Support\Defer\DeferredCallback */ function defer(?callable $callback = null, ?string $name = null, bool $always = false) @@ -516,12 +546,24 @@ function info($message, $context = []) } } +if (! function_exists('lang_path')) { + /** + * Get the path to the language folder. + * + * @param string $path + * @return string + */ + function lang_path($path = '') + { + return app()->langPath($path); + } +} + if (! function_exists('logger')) { /** * Log a debug message to the logs. * * @param string|null $message - * @param array $context * @return ($message is null ? \Illuminate\Log\LogManager : null) */ function logger($message = null, array $context = []) @@ -534,19 +576,6 @@ function logger($message = null, array $context = []) } } -if (! function_exists('lang_path')) { - /** - * Get the path to the language folder. - * - * @param string $path - * @return string - */ - function lang_path($path = '') - { - return app()->langPath($path); - } -} - if (! function_exists('logs')) { /** * Get a log driver instance. @@ -799,7 +828,6 @@ function rescue(callable $callback, $rescue = null, $report = true) * @template TClass of object * * @param string|class-string $name - * @param array $parameters * @return ($name is class-string ? TClass : mixed) */ function resolve($name, array $parameters = []) @@ -827,7 +855,6 @@ function resource_path($path = '') * * @param \Illuminate\Contracts\View\View|string|array|null $content * @param int $status - * @param array $headers * @return ($content is null ? \Illuminate\Contracts\Routing\ResponseFactory : \Illuminate\Http\Response) */ function response($content = null, $status = 200, array $headers = []) @@ -975,7 +1002,6 @@ function trans($key = null, $replace = [], $locale = null) * * @param string $key * @param \Countable|int|float|array $number - * @param array $replace * @param string|null $locale * @return string */ @@ -1041,10 +1067,6 @@ function url($path = null, $parameters = [], $secure = null) /** * Create a new Validator instance. * - * @param array|null $data - * @param array $rules - * @param array $messages - * @param array $attributes * @return ($data is null ? \Illuminate\Contracts\Validation\Factory : \Illuminate\Contracts\Validation\Validator) */ function validator(?array $data = null, array $rules = [], array $messages = [], array $attributes = []) diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json index 1935db7d4b58..7c464521a836 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json @@ -850,9 +850,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 7abcb3a459b8..28bde9944772 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -214,6 +214,13 @@ class PendingRequest 'query', ]; + /** + * The length at which request exceptions will be truncated. + * + * @var int<1, max>|false|null + */ + protected $truncateExceptionsAt = null; + /** * Create a new HTTP Client instance. * @@ -935,15 +942,20 @@ public function send(string $method, string $url, array $options = []) } } }); - } catch (ConnectException $e) { - $exception = new ConnectionException($e->getMessage(), 0, $e); - $request = new Request($e->getRequest()); + } catch (TransferException $e) { + if ($e instanceof ConnectException) { + $this->marshalConnectionException($e); + } - $this->factory?->recordRequestResponsePair($request, null); + if ($e instanceof RequestException && ! $e->hasResponse()) { + $this->marshalRequestExceptionWithoutResponse($e); + } - $this->dispatchConnectionFailedEvent($request, $exception); + if ($e instanceof RequestException && $e->hasResponse()) { + $this->marshalRequestExceptionWithResponse($e); + } - throw $exception; + throw $e; } }, $this->retryDelay ?? 100, function ($exception) use (&$shouldRetry) { $result = $shouldRetry ?? ($this->retryWhenCallback ? call_user_func($this->retryWhenCallback, $exception, $this, $this->request?->toPsrRequest()->getMethod()) : true); @@ -1424,7 +1436,15 @@ public function mergeOptions(...$options) */ protected function newResponse($response) { - return new Response($response); + return tap(new Response($response), function (Response $laravelResponse) { + if ($this->truncateExceptionsAt === null) { + return; + } + + $this->truncateExceptionsAt === false + ? $laravelResponse->dontTruncateExceptions() + : $laravelResponse->truncateExceptionsAt($this->truncateExceptionsAt); + }); } /** @@ -1517,6 +1537,85 @@ protected function dispatchConnectionFailedEvent(Request $request, ConnectionExc } } + /** + * Indicate that request exceptions should be truncated to the given length. + * + * @param int<1, max> $length + * @return $this + */ + public function truncateExceptionsAt(int $length) + { + $this->truncateExceptionsAt = $length; + + return $this; + } + + /** + * Indicate that request exceptions should not be truncated. + * + * @return $this + */ + public function dontTruncateExceptions() + { + $this->truncateExceptionsAt = false; + + return $this; + } + + /** + * Handle the given connection exception. + * + * @param \GuzzleHttp\Exception\ConnectException $e + * @return void + */ + protected function marshalConnectionException(ConnectException $e) + { + $exception = new ConnectionException($e->getMessage(), 0, $e); + + $this->factory?->recordRequestResponsePair( + $request = new Request($e->getRequest()), null + ); + + $this->dispatchConnectionFailedEvent($request, $exception); + + throw $exception; + } + + /** + * Handle the given request exception. + * + * @param \GuzzleHttp\Exception\RequestException $e + * @return void + */ + protected function marshalRequestExceptionWithoutResponse(RequestException $e) + { + $exception = new ConnectionException($e->getMessage(), 0, $e); + + $this->factory?->recordRequestResponsePair( + $request = new Request($e->getRequest()), null + ); + + $this->dispatchConnectionFailedEvent($request, $exception); + + throw $exception; + } + + /** + * Handle the given request exception. + * + * @param \GuzzleHttp\Exception\RequestException $e + * @return void + */ + protected function marshalRequestExceptionWithResponse(RequestException $e) + { + $this->factory?->recordRequestResponsePair( + new Request($e->getRequest()), + $response = $this->populateResponse($this->newResponse($e->getResponse())) + ); + + throw $response->toException() ?? new ConnectionException($e->getMessage(), 0, $e); + } + /** * Set the client instance. * diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index e69647bfb2bc..27f0899cb517 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -47,6 +47,13 @@ class Response implements ArrayAccess, Stringable */ public $transferStats; + /** + * The length at which request exceptions will be truncated. + * + * @var int<1, max>|false|null + */ + protected $truncateExceptionsAt = null; + /** * Create a new response instance. * @@ -297,7 +304,19 @@ public function toPsrResponse() public function toException() { if ($this->failed()) { - return new RequestException($this); + $originalTruncateAt = RequestException::$truncateAt; + + try { + if ($this->truncateExceptionsAt !== null) { + $this->truncateExceptionsAt === false + ? RequestException::dontTruncate() + : RequestException::truncateAt($this->truncateExceptionsAt); + } + + return new RequestException($this); + } finally { + RequestException::$truncateAt = $originalTruncateAt; + } } } @@ -395,6 +414,31 @@ public function throwIfServerError() return $this->serverError() ? $this->throw() : $this; } + /** + * Indicate that request exceptions should be truncated to the given length. + * + * @param int<1, max> $length + * @return $this + */ + public function truncateExceptionsAt(int $length) + { + $this->truncateExceptionsAt = $length; + + return $this; + } + + /** + * Indicate that request exceptions should not be truncated. + * + * @return $this + */ + public function dontTruncateExceptions() + { + $this->truncateExceptionsAt = false; + + return $this; + } + /** * Dump the content from the response. * diff --git a/src/Illuminate/Log/Context/Repository.php b/src/Illuminate/Log/Context/Repository.php index 5fcfc2710e25..a221409186cb 100644 --- a/src/Illuminate/Log/Context/Repository.php +++ b/src/Illuminate/Log/Context/Repository.php @@ -249,6 +249,42 @@ public function addHidden($key, #[\SensitiveParameter] $value = null) return $this; } + /** + * Add a context value if it does not exist yet, and return the value. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function remember($key, $value) + { + if ($this->has($key)) { + return $this->get($key); + } + + return tap(value($value), function ($value) use ($key) { + $this->add($key, $value); + }); + } + + /** + * Add a hidden context value if it does not exist yet, and return the value. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function rememberHidden($key, #[\SensitiveParameter] $value) + { + if ($this->hasHidden($key)) { + return $this->getHidden($key); + } + + return tap(value($value), function ($value) use ($key) { + $this->addHidden($key, $value); + }); + } + /** * Forget the given context key. * diff --git a/src/Illuminate/Mail/Attachment.php b/src/Illuminate/Mail/Attachment.php index f49d32cc1e78..8e2e87ed5496 100644 --- a/src/Illuminate/Mail/Attachment.php +++ b/src/Illuminate/Mail/Attachment.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Container\Container; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Traits\Macroable; use RuntimeException; @@ -79,6 +80,23 @@ public static function fromData(Closure $data, $name = null) ))->as($name); } + /** + * Create a mail attachment from an UploadedFile instance. + * + * @param \Illuminate\Http\UploadedFile $file + * @return static + */ + public static function fromUploadedFile(UploadedFile $file) + { + return new static(function ($attachment, $pathStrategy, $dataStrategy) use ($file) { + $attachment + ->as($file->getClientOriginalName()) + ->withMime($file->getMimeType() ?? $file->getClientMimeType()); + + return $dataStrategy(fn () => $file->get(), $attachment); + }); + } + /** * Create a mail attachment from a file in the default storage disk. * diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index afac565ec7a8..38318992c7f0 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -13,6 +13,7 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Support\Collection; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use Illuminate\Support\Traits\Conditionable; @@ -1371,7 +1372,7 @@ public function assertHasSubject($subject) */ public function assertSeeInHtml($string, $escape = true) { - $string = $escape ? e($string) : $string; + $string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string; [$html, $text] = $this->renderForAssertions(); @@ -1393,7 +1394,7 @@ public function assertSeeInHtml($string, $escape = true) */ public function assertDontSeeInHtml($string, $escape = true) { - $string = $escape ? e($string) : $string; + $string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string; [$html, $text] = $this->renderForAssertions(); @@ -1415,7 +1416,9 @@ public function assertDontSeeInHtml($string, $escape = true) */ public function assertSeeInOrderInHtml($strings, $escape = true) { - $strings = $escape ? array_map(e(...), $strings) : $strings; + $strings = $escape ? array_map(function ($string) { + return EncodedHtmlString::convert($string, withQuote: isset($this->markdown)); + }, $strings) : $strings; [$html, $text] = $this->renderForAssertions(); diff --git a/src/Illuminate/Mail/Markdown.php b/src/Illuminate/Mail/Markdown.php index 06d123ed3858..d99a232b7f0b 100644 --- a/src/Illuminate/Mail/Markdown.php +++ b/src/Illuminate/Mail/Markdown.php @@ -3,6 +3,7 @@ namespace Illuminate\Mail; use Illuminate\Contracts\View\Factory as ViewFactory; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use League\CommonMark\Environment\Environment; @@ -34,6 +35,13 @@ class Markdown */ protected $componentPaths = []; + /** + * Indicates if secure encoding should be enabled. + * + * @var bool + */ + protected static $withSecuredEncoding = false; + /** * Create a new Markdown renderer instance. * @@ -59,9 +67,37 @@ public function render($view, array $data = [], $inliner = null) { $this->view->flushFinderCache(); - $contents = $this->view->replaceNamespace( - 'mail', $this->htmlComponentPaths() - )->make($view, $data)->render(); + $bladeCompiler = $this->view + ->getEngineResolver() + ->resolve('blade') + ->getCompiler(); + + $contents = $bladeCompiler->usingEchoFormat( + 'new \Illuminate\Support\EncodedHtmlString(%s)', + function () use ($view, $data) { + if (static::$withSecuredEncoding === true) { + EncodedHtmlString::encodeUsing(function ($value) { + $replacements = [ + '[' => '\[', + '<' => '<', + '>' => '>', + ]; + + return str_replace(array_keys($replacements), array_values($replacements), $value); + }); + } + + try { + $contents = $this->view->replaceNamespace( + 'mail', $this->htmlComponentPaths() + )->make($view, $data)->render(); + } finally { + EncodedHtmlString::flushState(); + } + + return $contents; + } + ); if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) { $theme = $customTheme; @@ -72,7 +108,7 @@ public function render($view, array $data = [], $inliner = null) } return new HtmlString(($inliner ?: new CssToInlineStyles)->convert( - $contents, $this->view->make($theme, $data)->render() + str_replace('\[', '[', $contents), $this->view->make($theme, $data)->render() )); } @@ -100,20 +136,59 @@ public function renderText($view, array $data = []) * Parse the given Markdown text into HTML. * * @param string $text + * @param bool $encoded * @return \Illuminate\Support\HtmlString */ - public static function parse($text) + public static function parse($text, bool $encoded = false) { - $environment = new Environment([ + if ($encoded === false) { + return new HtmlString(static::converter()->convert($text)->getContent()); + } + + if (static::$withSecuredEncoding === true || $encoded === true) { + EncodedHtmlString::encodeUsing(function ($value) { + $replacements = [ + '[' => '\[', + '<' => '\<', + ]; + + $html = str_replace(array_keys($replacements), array_values($replacements), $value); + + return static::converter([ + 'html_input' => 'escape', + ])->convert($html)->getContent(); + }); + } + + $html = ''; + + try { + $html = static::converter()->convert($text)->getContent(); + } finally { + EncodedHtmlString::flushState(); + } + + return new HtmlString($html); + } + + /** + * Get a Markdown converter instance. + * + * @internal + * + * @param array $config + * @return \League\CommonMark\MarkdownConverter + */ + public static function converter(array $config = []) + { + $environment = new Environment(array_merge([ 'allow_unsafe_links' => false, - ]); + ], $config)); $environment->addExtension(new CommonMarkCoreExtension); $environment->addExtension(new TableExtension); - $converter = new MarkdownConverter($environment); - - return new HtmlString($converter->convert($text)->getContent()); + return new MarkdownConverter($environment); } /** @@ -185,4 +260,34 @@ public function getTheme() { return $this->theme; } + + /** + * Enable secured encoding when parsing Markdown. + * + * @return void + */ + public static function withSecuredEncoding() + { + static::$withSecuredEncoding = true; + } + + /** + * Disable secured encoding when parsing Markdown. + * + * @return void + */ + public static function withoutSecuredEncoding() + { + static::$withSecuredEncoding = false; + } + + /** + * Flush the class's global state. + * + * @return void + */ + public static function flushState() + { + static::$withSecuredEncoding = false; + } } diff --git a/src/Illuminate/Mail/Transport/ResendTransport.php b/src/Illuminate/Mail/Transport/ResendTransport.php index 0e690bf30b5a..9693eaf3a476 100644 --- a/src/Illuminate/Mail/Transport/ResendTransport.php +++ b/src/Illuminate/Mail/Transport/ResendTransport.php @@ -72,12 +72,19 @@ protected function doSend(SentMessage $message): void if ($email->getAttachments()) { foreach ($email->getAttachments() as $attachment) { $attachmentHeaders = $attachment->getPreparedHeaders(); + $contentType = $attachmentHeaders->get('Content-Type')->getBody(); $filename = $attachmentHeaders->getHeaderParameter('Content-Disposition', 'filename'); + if ($contentType == 'text/calendar') { + $content = $attachment->getBody(); + } else { + $content = str_replace("\r\n", '', $attachment->bodyToString()); + } + $item = [ - 'content_type' => $attachmentHeaders->get('Content-Type')->getBody(), - 'content' => str_replace("\r\n", '', $attachment->bodyToString()), + 'content_type' => $contentType, + 'content' => $content, 'filename' => $filename, ]; diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index 6f976a9e45dc..8df873951555 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -37,6 +37,7 @@ }, "suggest": { "aws/aws-sdk-php": "Required to use the SES mail driver (^3.322.9).", + "illuminate/http": "Required to create an attachment from an UploadedFile instance (^12.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", "symfony/http-client": "Required to use the Symfony API mail transports (^7.2).", "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", diff --git a/src/Illuminate/Mail/resources/views/html/button.blade.php b/src/Illuminate/Mail/resources/views/html/button.blade.php index 4a9bf7d00495..050e969d2130 100644 --- a/src/Illuminate/Mail/resources/views/html/button.blade.php +++ b/src/Illuminate/Mail/resources/views/html/button.blade.php @@ -12,7 +12,7 @@
-{{ $slot }} +{!! $slot !!}
diff --git a/src/Illuminate/Mail/resources/views/html/header.blade.php b/src/Illuminate/Mail/resources/views/html/header.blade.php index 56197f8d23f3..c47a260c56b2 100644 --- a/src/Illuminate/Mail/resources/views/html/header.blade.php +++ b/src/Illuminate/Mail/resources/views/html/header.blade.php @@ -5,7 +5,7 @@ @if (trim($slot) === 'Laravel') @else -{{ $slot }} +{!! $slot !!} @endif diff --git a/src/Illuminate/Mail/resources/views/html/layout.blade.php b/src/Illuminate/Mail/resources/views/html/layout.blade.php index d31a01de8630..0fa6b82f72b2 100644 --- a/src/Illuminate/Mail/resources/views/html/layout.blade.php +++ b/src/Illuminate/Mail/resources/views/html/layout.blade.php @@ -23,7 +23,7 @@ } } -{{ $head ?? '' }} +{!! $head ?? '' !!} @@ -31,7 +31,7 @@ -{{ $header ?? '' }} +{!! $header ?? '' !!} @@ -40,16 +40,16 @@ -{{ $footer ?? '' }} +{!! $footer ?? '' !!} diff --git a/src/Illuminate/Mail/resources/views/html/message.blade.php b/src/Illuminate/Mail/resources/views/html/message.blade.php index 1a874fc26de5..a16bace0a691 100644 --- a/src/Illuminate/Mail/resources/views/html/message.blade.php +++ b/src/Illuminate/Mail/resources/views/html/message.blade.php @@ -7,13 +7,13 @@ {{-- Body --}} -{{ $slot }} +{!! $slot !!} {{-- Subcopy --}} @isset($subcopy) -{{ $subcopy }} +{!! $subcopy !!} @endisset diff --git a/src/Illuminate/Notifications/NotificationSender.php b/src/Illuminate/Notifications/NotificationSender.php index 46ef9e88cf15..24819d5e85ce 100644 --- a/src/Illuminate/Notifications/NotificationSender.php +++ b/src/Illuminate/Notifications/NotificationSender.php @@ -12,6 +12,8 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\Localizable; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Exception\TransportException; use Throwable; class NotificationSender @@ -144,6 +146,8 @@ protected function preferredLocale($notifiable, $notification) * @param mixed $notification * @param string $channel * @return void + * + * @throws \Throwable */ protected function sendToNotifiable($notifiable, $id, $notification, $channel) { @@ -159,6 +163,10 @@ protected function sendToNotifiable($notifiable, $id, $notification, $channel) $response = $this->manager->driver($channel)->send($notifiable, $notification); } catch (Throwable $exception) { if (! $this->failedEventWasDispatched) { + if ($exception instanceof HttpTransportException) { + $exception = new TransportException($exception->getMessage(), $exception->getCode()); + } + $this->events->dispatch( new NotificationFailed($notifiable, $notification, $channel, ['exception' => $exception]) ); @@ -249,7 +257,11 @@ protected function queueNotification($notifiables, $notification) } $this->bus->dispatch( - (new SendQueuedNotifications($notifiable, $notification, [$channel])) + $this->manager->getContainer()->make(SendQueuedNotifications::class, [ + 'notifiables' => $notifiable, + 'notification' => $notification, + 'channels' => [$channel], + ]) ->onConnection($connection) ->onQueue($queue) ->delay(is_array($delay) ? ($delay[$channel] ?? null) : $delay) diff --git a/src/Illuminate/Pagination/AbstractCursorPaginator.php b/src/Illuminate/Pagination/AbstractCursorPaginator.php index 850f8b7fe0f9..e36d07ee26e2 100644 --- a/src/Illuminate/Pagination/AbstractCursorPaginator.php +++ b/src/Illuminate/Pagination/AbstractCursorPaginator.php @@ -14,6 +14,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\Tappable; +use Illuminate\Support\Traits\TransformsToResourceCollection; use Stringable; use Traversable; @@ -26,7 +27,7 @@ */ abstract class AbstractCursorPaginator implements Htmlable, Stringable { - use ForwardsCalls, Tappable; + use ForwardsCalls, Tappable, TransformsToResourceCollection; /** * All of the items being paginated. diff --git a/src/Illuminate/Pagination/AbstractPaginator.php b/src/Illuminate/Pagination/AbstractPaginator.php index b27830f84ecc..8be1013a8d93 100644 --- a/src/Illuminate/Pagination/AbstractPaginator.php +++ b/src/Illuminate/Pagination/AbstractPaginator.php @@ -353,8 +353,10 @@ public function lastItem() /** * Transform each item in the slice of items using a callback. * - * @param callable $callback - * @return $this + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return $this */ public function through(callable $callback) { diff --git a/src/Illuminate/Pagination/Paginator.php b/src/Illuminate/Pagination/Paginator.php index 489b58fc2a8f..69c9ad2f1385 100644 --- a/src/Illuminate/Pagination/Paginator.php +++ b/src/Illuminate/Pagination/Paginator.php @@ -153,6 +153,7 @@ public function toArray() { return [ 'current_page' => $this->currentPage(), + 'current_page_url' => $this->url($this->currentPage()), 'data' => $this->items->toArray(), 'first_page_url' => $this->url(1), 'from' => $this->firstItem(), diff --git a/src/Illuminate/Pagination/resources/views/simple-bootstrap-5.blade.php b/src/Illuminate/Pagination/resources/views/simple-bootstrap-5.blade.php index a89005ee7d81..7006add80569 100644 --- a/src/Illuminate/Pagination/resources/views/simple-bootstrap-5.blade.php +++ b/src/Illuminate/Pagination/resources/views/simple-bootstrap-5.blade.php @@ -1,5 +1,5 @@ @if ($paginator->hasPages()) -