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/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 6102e3295697..9b65a09089f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,249 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.14.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.21.0...12.x) + +## [v12.21.0](https://github.com/laravel/framework/compare/v12.20.0...v12.21.0) - 2025-07-22 + +* fix(vite): #55793 add explicit as-script to link tag for script modul… by [@midsonlajeanty](https://github.com/midsonlajeanty) in https://github.com/laravel/framework/pull/55794 +* [12.x] Allow globally disabling Factory parent relationships via `Factory::dontExpandRelationshipsByDefault()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56154 +* [12.x] Adds checking if a value is between two columns by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/56119 +* [12.x] Ensure database connection is always restored by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56258 +* [12.x] Fix handling of `Htmlable` objects in `Js::convertDataToJavaScriptExpression()` by [@jj15asmr](https://github.com/jj15asmr) in https://github.com/laravel/framework/pull/56253 +* Reduce meaningless intermediate variables. by [@LjjGit](https://github.com/LjjGit) in https://github.com/laravel/framework/pull/56265 +* [12.x] Improve typehints for `AbstractCursorPaginator@through()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56267 +* Use `Date` facade instead of `time()` for `password_confirmed_at` check by [@dylanbr](https://github.com/dylanbr) in https://github.com/laravel/framework/pull/56270 +* [12.x] fix: Collection::transform() and Paginator::through() return types by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56273 +* [12.x] Merge 11.x into 12.x by [@u01jmg3](https://github.com/u01jmg3) in https://github.com/laravel/framework/pull/56289 +* [12.x] Reduce meaningless intermediate variables by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56288 +* [12.x] Refactor build Method to Use Null Coalescing Assignment for Default C… by [@Ashot1995](https://github.com/Ashot1995) in https://github.com/laravel/framework/pull/56283 +* [12.x] minor code formatting improvements by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56296 +* [12.x] Use more specific route binding exception message for child routes by [@jessekoerhuis](https://github.com/jessekoerhuis) in https://github.com/laravel/framework/pull/56298 +* [12.x] Fix Possible Undefined Variables by [@calfc](https://github.com/calfc) in https://github.com/laravel/framework/pull/56292 +* [12.x] Fix: Ensure scheduler `dailyAt()` method parses minutes and ignores seconds when seconds are provided by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56308 +* [12.x] Allows for strict boolean validation by [@peterfox](https://github.com/peterfox) in https://github.com/laravel/framework/pull/56313 +* Improve `SeedCommand` console output by [@Jehong-Ahn](https://github.com/Jehong-Ahn) in https://github.com/laravel/framework/pull/56310 +* [12.x] Add unified enum support across framework docs by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56271 +* [12.x] Allows for strict numeric validation by [@peterfox](https://github.com/peterfox) in https://github.com/laravel/framework/pull/56328 +* [12.x] Update PHPDoc annotations in `Validation` by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/56321 +* [12.x] Add operator class support for PostgreSQL GiST spatial indexes by [@joteejotee](https://github.com/joteejotee) in https://github.com/laravel/framework/pull/56324 +* Fix multipart array value parsing in HTTP client (#55732) by [@joteejotee](https://github.com/joteejotee) in https://github.com/laravel/framework/pull/56302 +* Fixes bug with ShouldBeUniqueUntilProcessing locks getting stuck due to Middleware by [@TWithers](https://github.com/TWithers) in https://github.com/laravel/framework/pull/56318 +* [12.x] add prompts based expectations to PendingCommand by [@BinaryKitten](https://github.com/BinaryKitten) in https://github.com/laravel/framework/pull/56260 +* [12.x] Add Singleton and Scoped attributes to Container by [@riasvdv](https://github.com/riasvdv) in https://github.com/laravel/framework/pull/56334 +* Fix unsetting model castable attribute when cast to object (#56335) by [@guram-vashakidze](https://github.com/guram-vashakidze) in https://github.com/laravel/framework/pull/56343 +* [12.x] Fix/memory improvement by [@CharrafiMed](https://github.com/CharrafiMed) in https://github.com/laravel/framework/pull/56345 +* [12.x] Add hasMailer method to the mailable class by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/56340 +* [12.x] Consistent use of `mb_split()` to split strings into words by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56338 +* [12.x] Add toStringable to Uri by [@Kyrch](https://github.com/Kyrch) in https://github.com/laravel/framework/pull/56359 +* [12.x] Fix PHPStan Integrations by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56369 +* Add 'isEmpty' and 'isNotEmpty' to Fluent by [@cworreschk](https://github.com/cworreschk) in https://github.com/laravel/framework/pull/56370 +* [12.x] Add mergeMetadata method to the Mailable class by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/56376 +* Add 'dontReportUsing' to filter exceptions to be reported by [@pelmered](https://github.com/pelmered) in https://github.com/laravel/framework/pull/56361 + +## [v12.20.0](https://github.com/laravel/framework/compare/v12.19.3...v12.20.0) - 2025-07-08 + +* [12.x] Pass TransportException to NotificationFailed event by [@hackel](https://github.com/hackel) in https://github.com/laravel/framework/pull/56061 +* [12.x] use `offset()` in place of `skip()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56081 +* [12.x] use `limit()` in place of `take()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56080 +* [12.x] Display job queue names when running queue:work with --verbose option by [@seriquynh](https://github.com/seriquynh) in https://github.com/laravel/framework/pull/56086 +* [12.x] use `offset()` and `limit()` in tests by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56089 +* [12.x] Localize “Pagination Navigation” aria-label by [@andylolz](https://github.com/andylolz) in https://github.com/laravel/framework/pull/56103 +* [12.x] Enhance the test coverage for Pipeline::through() by [@azim-kordpour](https://github.com/azim-kordpour) in https://github.com/laravel/framework/pull/56100 +* [12.x] Added `JsonSerializable` interface to `Uri` Class by [@devajmeireles](https://github.com/devajmeireles) in https://github.com/laravel/framework/pull/56097 +* [12.x] Display job connection name when running queue:work with --verbose option by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56095 +* [12.x] Fix PHPDoc for Arr::sole method by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56096 +* [12.x] when a method returns `$this` set the return type to `static` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56092 +* [12.x] Use `int<0, max>` as docblock return type for database operations that return a count by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56117 +* [12.x] Add missing [@throws](https://github.com/throws) annotation to Number by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56116 +* [12.x] Correct PHPDoc for Arr::sole callable type to avoid return type ambiguity by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56108 +* Change return types of through (pagination) and transform (collection) by [@glamorous](https://github.com/glamorous) in https://github.com/laravel/framework/pull/56105 +* [12.x] Add maintenance mode facade for easier driver extension by [@ziadoz](https://github.com/ziadoz) in https://github.com/laravel/framework/pull/56090 +* [12.x] Cache isSoftDeletable(), isPrunable(), and isMassPrunable() directly in model by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56078 +* [12.x] Throws not throw by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56120 +* [12.x] Fix [@param](https://github.com/param) docblock to allow string by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56121 +* [11.x] Pass the limiter to the when & report callbacks by [@jimmypuckett](https://github.com/jimmypuckett) in https://github.com/laravel/framework/pull/56129 +* [12.x] remove the "prefix" option for cache password resets by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56127 +* [12.x] Make Model::currentEncrypter public by [@JaZo](https://github.com/JaZo) in https://github.com/laravel/framework/pull/56130 +* [12.x] Add throws docblock by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56137 +* [12.x] Narrow integer range for `Collection` methods by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56135 +* [12.x] Allows using `--model` and `--except` via `PruneCommand` command by [@hosni](https://github.com/hosni) in https://github.com/laravel/framework/pull/56140 +* [12.x] Support Passing `Htmlable` Instances to `Js::from()` by [@jj15asmr](https://github.com/jj15asmr) in https://github.com/laravel/framework/pull/56159 +* #56124 Properly escape column defaults by [@asmecher](https://github.com/asmecher) in https://github.com/laravel/framework/pull/56158 +* [12.x] Return early on belongs-to-many relationship `syncWithoutDetaching` method when empty values are given by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/56157 +* [12.x] Add fakeFor and fakeExceptFor methods to Queue facade by [@MrPunyapal](https://github.com/MrPunyapal) in https://github.com/laravel/framework/pull/56149 +* [11.x] Backport test fixes by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56183 +* Revert "[11.x] Pass the limiter to the when & report callbacks" by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56184 +* Add failWhen method to ThrottlesExceptions job middleware by [@michaeldzjap](https://github.com/michaeldzjap) in https://github.com/laravel/framework/pull/56180 +* [12.x] Update Castable contract to accept string array by [@hosmelq](https://github.com/hosmelq) in https://github.com/laravel/framework/pull/56177 +* Feature: doesntStartWith() and doesntEndWith() string methods by [@balboacodes](https://github.com/balboacodes) in https://github.com/laravel/framework/pull/56168 +* [12.x] Add context remember functions by [@btaskew](https://github.com/btaskew) in https://github.com/laravel/framework/pull/56156 +* [12.x] Fix queue fake cleanup to always restore original queue manager by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56165 +* [12.x] Pass the limiter to the when & report callbacks by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56187 +* [12.x] Add `Closure`-support to `$key`/`$value` in Collection `pluck()` method by [@ralphjsmit](https://github.com/ralphjsmit) in https://github.com/laravel/framework/pull/56188 +* [12.x] Add `collection()` to Config repository by [@KennedyTedesco](https://github.com/KennedyTedesco) in https://github.com/laravel/framework/pull/56200 +* Add int to allowed types of value in DatabaseRule by [@vkarchevskyi](https://github.com/vkarchevskyi) in https://github.com/laravel/framework/pull/56199 +* [12.x] Fix Event fake cleanup to always restore original event dispatcher by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56189 +* [12.x] Align PHPDoc style in Number::parseFloat with the rest of the class by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56206 +* [12.x] Inconsistent use of [@return](https://github.com/return) type by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56207 +* [12.x] Resolve issue with Factory make when automatic eager loading by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/56211 +* [12.x] Refactor driver initialization using null coalescing assignment in Manager by [@Ashot1995](https://github.com/Ashot1995) in https://github.com/laravel/framework/pull/56210 +* [12.x] Add URL signature macros to `Request` docblock by [@duncanmcclean](https://github.com/duncanmcclean) in https://github.com/laravel/framework/pull/56230 +* [12.x] Update PHPDoc for dataForSometimesIteration by [@mrvipchien](https://github.com/mrvipchien) in https://github.com/laravel/framework/pull/56229 +* [12.x] Avoid unnecessary filtering when no callback is provided by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56225 +* [12.x] Make `Fluent` class iterable by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56218 +* Improve Mailable assertion error messages with expected vs actual values by [@ahinkle](https://github.com/ahinkle) in https://github.com/laravel/framework/pull/56221 +* [12.x] Add `@​context` Blade directive by [@martinbean](https://github.com/martinbean) in https://github.com/laravel/framework/pull/56146 +* [12.x] fix: AsCommand properties not being set on commands by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56235 +* [12.x] Ensure `withLocale` and `withCurrency` always restore previous state by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/56234 + +## [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 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..775c43f91914 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; @@ -17,6 +18,9 @@ use function Illuminate\Support\enum_value; +/** + * @template TClass of object + */ class Gate implements GateContract { use HandlesAuthorization; @@ -325,7 +329,7 @@ public function after(callable $callback) * Determine if all of the given abilities should be granted for the current user. * * @param iterable|\UnitEnum|string $ability - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return bool */ public function allows($ability, $arguments = []) @@ -337,7 +341,7 @@ public function allows($ability, $arguments = []) * Determine if any of the given abilities should be denied for the current user. * * @param iterable|\UnitEnum|string $ability - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return bool */ public function denies($ability, $arguments = []) @@ -349,7 +353,7 @@ public function denies($ability, $arguments = []) * Determine if all of the given abilities should be granted for the current user. * * @param iterable|\UnitEnum|string $abilities - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return bool */ public function check($abilities, $arguments = []) @@ -363,7 +367,7 @@ public function check($abilities, $arguments = []) * Determine if any one of the given abilities should be granted for the current user. * * @param iterable|\UnitEnum|string $abilities - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return bool */ public function any($abilities, $arguments = []) @@ -375,7 +379,7 @@ public function any($abilities, $arguments = []) * Determine if all of the given abilities should be denied for the current user. * * @param iterable|\UnitEnum|string $abilities - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return bool */ public function none($abilities, $arguments = []) @@ -387,7 +391,7 @@ public function none($abilities, $arguments = []) * Determine if the given ability should be granted for the current user. * * @param \UnitEnum|string $ability - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return \Illuminate\Auth\Access\Response * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -401,7 +405,7 @@ public function authorize($ability, $arguments = []) * Inspect the user for the given ability. * * @param \UnitEnum|string $ability - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return \Illuminate\Auth\Access\Response */ public function inspect($ability, $arguments = []) @@ -425,7 +429,7 @@ public function inspect($ability, $arguments = []) * Get the raw result from the authorization callback. * * @param string $ability - * @param array|mixed $arguments + * @param array{class-string|TClass, ...} $arguments * @return mixed * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -669,6 +673,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 +692,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/Access/HandlesAuthorization.php b/src/Illuminate/Auth/Access/HandlesAuthorization.php index ed2162459a44..f1109edc2d63 100644 --- a/src/Illuminate/Auth/Access/HandlesAuthorization.php +++ b/src/Illuminate/Auth/Access/HandlesAuthorization.php @@ -20,7 +20,7 @@ protected function allow($message = null, $code = null) * Throws an unauthorized exception. * * @param string|null $message - * @param mixed|null $code + * @param mixed $code * @return \Illuminate\Auth\Access\Response */ protected function deny($message = null, $code = null) 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/Middleware/RequirePassword.php b/src/Illuminate/Auth/Middleware/RequirePassword.php index 8ac6f8af66d4..06fa9698efb1 100644 --- a/src/Illuminate/Auth/Middleware/RequirePassword.php +++ b/src/Illuminate/Auth/Middleware/RequirePassword.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Contracts\Routing\UrlGenerator; +use Illuminate\Support\Facades\Date; class RequirePassword { @@ -92,7 +93,7 @@ public function handle($request, Closure $next, $redirectToRoute = null, $passwo */ protected function shouldConfirmPassword($request, $passwordTimeoutSeconds = null) { - $confirmedAt = time() - $request->session()->get('auth.password_confirmed_at', 0); + $confirmedAt = Date::now()->unix() - $request->session()->get('auth.password_confirmed_at', 0); return $confirmedAt > ($passwordTimeoutSeconds ?? $this->passwordTimeout); } diff --git a/src/Illuminate/Auth/Notifications/VerifyEmail.php b/src/Illuminate/Auth/Notifications/VerifyEmail.php index 7a5cf916449d..7c4efc31ca51 100644 --- a/src/Illuminate/Auth/Notifications/VerifyEmail.php +++ b/src/Illuminate/Auth/Notifications/VerifyEmail.php @@ -21,7 +21,7 @@ class VerifyEmail extends Notification /** * The callback that should be used to build the mail message. * - * @var \Closure|null + * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable)|null */ public static $toMailCallback; @@ -104,7 +104,7 @@ public static function createUrlUsing($callback) /** * Set a callback that should be used when building the notification mail message. * - * @param \Closure $callback + * @param \Closure(mixed, string): (\Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable) $callback * @return void */ public static function toMailUsing($callback) 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 @@ +broadcastConnection = is_null($connection) ? [null] : Arr::wrap($connection); diff --git a/src/Illuminate/Broadcasting/PendingBroadcast.php b/src/Illuminate/Broadcasting/PendingBroadcast.php index 0d1298e07111..6f5ee39f0035 100644 --- a/src/Illuminate/Broadcasting/PendingBroadcast.php +++ b/src/Illuminate/Broadcasting/PendingBroadcast.php @@ -4,6 +4,8 @@ use Illuminate\Contracts\Events\Dispatcher; +use function Illuminate\Support\enum_value; + class PendingBroadcast { /** @@ -35,13 +37,13 @@ public function __construct(Dispatcher $events, $event) /** * Broadcast the event using a specific broadcaster. * - * @param string|null $connection + * @param \UnitEnum|string|null $connection * @return $this */ public function via($connection = null) { if (method_exists($this->event, 'broadcastVia')) { - $this->event->broadcastVia($connection); + $this->event->broadcastVia(enum_value($connection)); } return $this; diff --git a/src/Illuminate/Bus/DatabaseBatchRepository.php b/src/Illuminate/Bus/DatabaseBatchRepository.php index 31bd39878c57..a2c56cc13ea2 100644 --- a/src/Illuminate/Bus/DatabaseBatchRepository.php +++ b/src/Illuminate/Bus/DatabaseBatchRepository.php @@ -59,7 +59,7 @@ public function get($limit = 50, $before = null) { return $this->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/Bus/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index 0107b9e5acd4..01bf5ec457d7 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -150,7 +150,7 @@ public function findBatch(string $batchId) /** * Create a new batch of queueable jobs. * - * @param \Illuminate\Support\Collection|array|mixed $jobs + * @param \Illuminate\Support\Collection|mixed $jobs * @return \Illuminate\Bus\PendingBatch */ public function batch($jobs) @@ -161,10 +161,10 @@ public function batch($jobs) /** * Create a new chain of queueable jobs. * - * @param \Illuminate\Support\Collection|array $jobs + * @param \Illuminate\Support\Collection|array|null $jobs * @return \Illuminate\Foundation\Bus\PendingChain */ - public function chain($jobs) + public function chain($jobs = null) { $jobs = Collection::wrap($jobs); $jobs = ChainedBatch::prepareNestedBatches($jobs); @@ -187,7 +187,7 @@ public function hasCommandHandler($command) * Retrieve the handler for a command. * * @param mixed $command - * @return bool|mixed + * @return mixed */ public function getCommandHandler($command) { diff --git a/src/Illuminate/Bus/Queueable.php b/src/Illuminate/Bus/Queueable.php index b8a439dad2ac..917f6540995e 100644 --- a/src/Illuminate/Bus/Queueable.php +++ b/src/Illuminate/Bus/Queueable.php @@ -204,11 +204,9 @@ public function through($middleware) */ public function chain($chain) { - $jobs = ChainedBatch::prepareNestedBatches(new Collection($chain)); - - $this->chained = $jobs->map(function ($job) { - return $this->serializeJob($job); - })->all(); + $this->chained = ChainedBatch::prepareNestedBatches(new Collection($chain)) + ->map(fn ($job) => $this->serializeJob($job)) + ->all(); return $this; } 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..0c25ddc01f74 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 */ @@ -444,7 +446,30 @@ public function getConnection() } /** - * Specify the name of the connection that should be used to manage locks. + * Set the underlying database connection. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @return $this + */ + public function setConnection($connection) + { + $this->connection = $connection; + + return $this; + } + + /** + * Get the connection used to manage locks. + * + * @return \Illuminate\Database\ConnectionInterface + */ + public function getLockConnection() + { + return $this->lockConnection; + } + + /** + * Specify the connection that should be used to manage locks. * * @param \Illuminate\Database\ConnectionInterface $connection * @return $this 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..a6273b46043a 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. * @@ -527,6 +552,42 @@ public static function hasAny($array, $keys) return false; } + /** + * Determine if all items pass the given truth test. + * + * @param iterable $array + * @param (callable(mixed, array-key): bool) $callback + * @return bool + */ + public static function every($array, callable $callback) + { + foreach ($array as $key => $value) { + if (! $callback($value, $key)) { + return false; + } + } + + return true; + } + + /** + * Determine if some items pass the given truth test. + * + * @param iterable $array + * @param (callable(mixed, array-key): bool) $callback + * @return bool + */ + public static function some($array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return true; + } + } + + return false; + } + /** * Get an integer item from an array using "dot" notation. */ @@ -534,7 +595,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 +723,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 +734,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 +744,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 +762,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 +989,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 +1100,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 +1123,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..ce683bee94d1 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)); } @@ -224,6 +228,19 @@ public function doesntContain($key, $operator = null, $value = null) return ! $this->contains(...func_get_args()); } + /** + * Determine if an item is not contained in the enumerable, using strict comparison. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContainStrict($key, $operator = null, $value = null) + { + return ! $this->containsStrict(...func_get_args()); + } + /** * Cross join with the given lists, returning all possible permutations. * @@ -1232,7 +1249,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 +1742,12 @@ public function takeWhile($value) /** * Transform each item in the collection using a callback. * - * @param callable(TValue, TKey): TValue $callback + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback * @return $this + * + * @phpstan-this-out static */ public function transform(callable $callback) { @@ -1838,7 +1859,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..14665acd5312 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -287,6 +287,19 @@ public function doesntContain($key, $operator = null, $value = null) return ! $this->contains(...func_get_args()); } + /** + * Determine if an item is not contained in the enumerable, using strict comparison. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContainStrict($key, $operator = null, $value = null) + { + return ! $this->containsStrict(...func_get_args()); + } + /** * Cross join the given iterables, returning all possible permutations. * @@ -777,12 +790,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; @@ -1762,9 +1779,9 @@ public function zip($items) $iterables = func_get_args(); return new static(function () use ($iterables) { - $iterators = (new Collection($iterables))->map(function ($iterable) { - return $this->makeIterator($iterable); - })->prepend($this->getIterator()); + $iterators = (new Collection($iterables)) + ->map(fn ($iterable) => $this->makeIterator($iterable)) + ->prepend($this->getIterator()); while ($iterators->contains->valid()) { yield new static($iterators->map->current()); @@ -1869,7 +1886,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/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index c11c9c434d89..b3c137320148 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -508,8 +508,8 @@ public function forPage($page, $perPage) * Partition the collection into two arrays using the given callback or key. * * @param (callable(TValue, TKey): bool)|TValue|string $key - * @param TValue|string|null $operator - * @param TValue|null $value + * @param mixed $operator + * @param mixed $value * @return static, static> */ public function partition($key, $operator = null, $value = null) 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 4729a5441a13..6263d85341bd 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -147,7 +147,7 @@ public static function forgetBootstrappers() /** * Run an Artisan console command by name. * - * @param string $command + * @param \Symfony\Component\Console\Command\Command|string $command * @param array $parameters * @param \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer * @return int @@ -170,7 +170,7 @@ public function call($command, array $parameters = [], $outputBuffer = null) /** * Parse the incoming Artisan command and its input. * - * @param string $command + * @param \Symfony\Component\Console\Command\Command|string $command * @param array $parameters * @return array */ @@ -179,6 +179,10 @@ protected function parseCommand($command, $parameters) if (is_subclass_of($command, SymfonyCommand::class)) { $callingClass = true; + if (is_object($command)) { + $command = get_class($command); + } + $command = $this->laravel->make($command)->getName(); } @@ -205,6 +209,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. * diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index e4be18364599..607cfaa07dbd 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -45,16 +45,16 @@ class Command extends SymfonyCommand /** * The console command description. * - * @var string|null + * @var string */ - protected $description; + protected $description = ''; /** * The console command help text. * * @var string */ - protected $help; + protected $help = ''; /** * Indicates whether the command should be shown in the Artisan command list. @@ -101,13 +101,13 @@ 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 { - $this->setDescription((string) $this->description); + if (! empty($this->description)) { + $this->setDescription($this->description); } - $this->setHelp((string) $this->help); + if (! empty($this->help)) { + $this->setHelp($this->help); + } $this->setHidden($this->isHidden()); @@ -263,7 +263,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/CacheSchedulingMutex.php b/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php index 439e5bea3790..5202ef2535b7 100644 --- a/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php +++ b/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php @@ -3,7 +3,9 @@ namespace Illuminate\Console\Scheduling; use DateTimeInterface; +use Illuminate\Cache\DynamoDbStore; use Illuminate\Contracts\Cache\Factory as Cache; +use Illuminate\Contracts\Cache\LockProvider; class CacheSchedulingMutex implements SchedulingMutex, CacheAware { @@ -40,8 +42,16 @@ public function __construct(Cache $cache) */ public function create(Event $event, DateTimeInterface $time) { + $mutexName = $event->mutexName().$time->format('Hi'); + + if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) { + return $this->cache->store($this->store)->getStore() + ->lock($mutexName, 3600) + ->acquire(); + } + return $this->cache->store($this->store)->add( - $event->mutexName().$time->format('Hi'), true, 3600 + $mutexName, true, 3600 ); } @@ -54,9 +64,26 @@ public function create(Event $event, DateTimeInterface $time) */ public function exists(Event $event, DateTimeInterface $time) { - return $this->cache->store($this->store)->has( - $event->mutexName().$time->format('Hi') - ); + $mutexName = $event->mutexName().$time->format('Hi'); + + if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) { + return ! $this->cache->store($this->store)->getStore() + ->lock($mutexName, 3600) + ->get(fn () => true); + } + + return $this->cache->store($this->store)->has($mutexName); + } + + /** + * Determine if the given store should use locks for cache event mutexes. + * + * @param \Illuminate\Contracts\Cache\Store $store + * @return bool + */ + protected function shouldUseLocks($store) + { + return $store instanceof LockProvider && ! $store instanceof DynamoDbStore; } /** diff --git a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php index 619d852e4817..f0f6678d79db 100644 --- a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php +++ b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php @@ -5,6 +5,8 @@ use Illuminate\Support\Carbon; use InvalidArgumentException; +use function Illuminate\Support\enum_value; + trait ManagesFrequencies { /** @@ -345,7 +347,7 @@ public function dailyAt($time) $segments = explode(':', $time); return $this->hourBasedSchedule( - count($segments) === 2 ? (int) $segments[1] : '0', + count($segments) >= 2 ? (int) $segments[1] : '0', (int) $segments[0] ); } @@ -639,12 +641,12 @@ public function days($days) /** * Set the timezone the date should be evaluated on. * - * @param \DateTimeZone|string $timezone + * @param \UnitEnum|\DateTimeZone|string $timezone * @return $this */ public function timezone($timezone) { - $this->timezone = $timezone; + $this->timezone = enum_value($timezone); return $this; } diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 17de97bad8cb..d2c835d59757 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -18,6 +18,9 @@ use Illuminate\Support\ProcessUtils; use Illuminate\Support\Traits\Macroable; use RuntimeException; +use Symfony\Component\Console\Command\Command as SymfonyCommand; + +use function Illuminate\Support\enum_value; /** * @mixin \Illuminate\Console\Scheduling\PendingEventAttributes @@ -147,12 +150,22 @@ public function call($callback, array $parameters = []) /** * Add a new Artisan command event to the schedule. * - * @param string $command + * @param \Symfony\Component\Console\Command\Command|string $command * @param array $parameters * @return \Illuminate\Console\Scheduling\Event */ public function command($command, array $parameters = []) { + if ($command instanceof SymfonyCommand) { + $command = get_class($command); + + $command = Container::getInstance()->make($command); + + return $this->exec( + Application::formatCommandString($command->getName()), $parameters, + )->description($command->getDescription()); + } + if (class_exists($command)) { $command = Container::getInstance()->make($command); @@ -170,14 +183,17 @@ public function command($command, array $parameters = []) * Add a new job callback event to the schedule. * * @param object|string $job - * @param string|null $queue - * @param string|null $connection + * @param \UnitEnum|string|null $queue + * @param \UnitEnum|string|null $connection * @return \Illuminate\Console\Scheduling\CallbackEvent */ public function job($job, $queue = null, $connection = null) { $jobName = $job; + $queue = enum_value($queue); + $connection = enum_value($connection); + if (! is_string($job)) { $jobName = method_exists($job, 'displayName') ? $job->displayName() diff --git a/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php b/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php index 575e590623b9..2fee1ac8fac3 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleFinishCommand.php @@ -40,12 +40,12 @@ class ScheduleFinishCommand extends Command */ public function handle(Schedule $schedule) { - (new Collection($schedule->events()))->filter(function ($value) { - return $value->mutexName() == $this->argument('id'); - })->each(function ($event) { - $event->finish($this->laravel, $this->argument('code')); + (new Collection($schedule->events())) + ->filter(fn ($value) => $value->mutexName() == $this->argument('id')) + ->each(function ($event) { + $event->finish($this->laravel, $this->argument('code')); - $this->laravel->make(Dispatcher::class)->dispatch(new ScheduledBackgroundTaskFinished($event)); - }); + $this->laravel->make(Dispatcher::class)->dispatch(new ScheduledBackgroundTaskFinished($event)); + }); } } 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/Bind.php b/src/Illuminate/Container/Attributes/Bind.php new file mode 100644 index 000000000000..3a9013546310 --- /dev/null +++ b/src/Illuminate/Container/Attributes/Bind.php @@ -0,0 +1,53 @@ + + */ + public array $environments = []; + + /** + * Create a new attribute instance. + * + * @param class-string $concrete + * @param non-empty-array|non-empty-string $environments + * + * @throws \InvalidArgumentException + */ + public function __construct( + string $concrete, + string|array $environments = ['*'], + ) { + $environments = array_filter(is_array($environments) ? $environments : [$environments]); + + if ($environments === []) { + throw new InvalidArgumentException('The environment property must be set and cannot be empty.'); + } + + $this->concrete = $concrete; + + $this->environments = array_map(fn ($environment) => match (true) { + $environment instanceof BackedEnum => $environment->value, + $environment instanceof UnitEnum => $environment->name, + default => $environment, + }, $environments); + } +} 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/Attributes/Scoped.php b/src/Illuminate/Container/Attributes/Scoped.php new file mode 100644 index 000000000000..1cf04e08d4b8 --- /dev/null +++ b/src/Illuminate/Container/Attributes/Scoped.php @@ -0,0 +1,10 @@ + + */ + protected $checkedForAttributeBindings = []; + /** * All of the registered rebound callbacks. * @@ -176,6 +186,13 @@ class Container implements ArrayAccess, ContainerContract */ protected $afterResolvingAttributeCallbacks = []; + /** + * The callback used to determine the container's environment. + * + * @var (callable(array|string): bool|string)|null + */ + protected $environmentResolver = null; + /** * Define a contextual binding. * @@ -252,9 +269,33 @@ public function resolved($abstract) */ public function isShared($abstract) { - return isset($this->instances[$abstract]) || - (isset($this->bindings[$abstract]['shared']) && - $this->bindings[$abstract]['shared'] === true); + if (isset($this->instances[$abstract])) { + return true; + } + + if (isset($this->bindings[$abstract]['shared']) && $this->bindings[$abstract]['shared'] === true) { + return true; + } + + if (! class_exists($abstract)) { + return false; + } + + $reflection = new ReflectionClass($abstract); + + if (! empty($reflection->getAttributes(Singleton::class))) { + return true; + } + + if (! empty($reflection->getAttributes(Scoped::class))) { + if (! in_array($abstract, $this->scopedInstances, true)) { + $this->scopedInstances[] = $abstract; + } + + return true; + } + + return false; } /** @@ -332,7 +373,7 @@ protected function getClosure($abstract, $concrete) } return $container->resolve( - $concrete, $parameters, $raiseEvents = false + $concrete, $parameters, raiseEvents: false ); }; } @@ -649,6 +690,8 @@ public function alias($abstract, $alias) throw new LogicException("[{$abstract}] is aliased to itself."); } + $this->removeAbstractAlias($alias); + $this->aliases[$alias] = $abstract; $this->abstractAliases[$abstract][] = $alias; @@ -935,7 +978,65 @@ protected function getConcrete($abstract) return $this->bindings[$abstract]['concrete']; } - return $abstract; + if ($this->environmentResolver === null || + ($this->checkedForAttributeBindings[$abstract] ?? false) || ! is_string($abstract)) { + return $abstract; + } + + $attributes = []; + + try { + $attributes = (new ReflectionClass($abstract))->getAttributes(Bind::class); + } catch (ReflectionException) { + } + + $this->checkedForAttributeBindings[$abstract] = true; + + if ($attributes === []) { + return $abstract; + } + + return $this->getConcreteBindingFromAttributes($abstract, $attributes); + } + + /** + * Get the concrete binding for an abstract from the Bind attribute. + * + * @param string $abstract + * @param array> $reflectedAttributes + * @return mixed + */ + protected function getConcreteBindingFromAttributes($abstract, $reflectedAttributes) + { + $concrete = $maybeConcrete = null; + + foreach ($reflectedAttributes as $reflectedAttribute) { + $instance = $reflectedAttribute->newInstance(); + + if ($instance->environments === ['*']) { + $maybeConcrete = $instance->concrete; + + continue; + } + + if ($this->currentEnvironmentIs($instance->environments)) { + $concrete = $instance->concrete; + + break; + } + } + + if ($maybeConcrete !== null && $concrete === null) { + $concrete = $maybeConcrete; + } + + if ($concrete === null) { + return $abstract; + } + + $this->bind($abstract, $concrete); + + return $this->bindings[$abstract]['concrete']; } /** @@ -1595,6 +1696,30 @@ public function forgetScopedInstances() } } + /** + * Set the callback which determines the current container environment. + * + * @param (callable(array|string): bool|string)|null $callback + * @return void + */ + public function resolveEnvironmentUsing(?callable $callback) + { + $this->environmentResolver = $callback; + } + + /** + * Determine the environment for the container. + * + * @param array|string $environments + * @return bool + */ + public function currentEnvironmentIs($environments) + { + return $this->environmentResolver === null + ? false + : call_user_func($this->environmentResolver, $environments); + } + /** * Flush the container of all bindings and resolved instances. * @@ -1608,6 +1733,7 @@ public function flush() $this->instances = []; $this->abstractAliases = []; $this->scopedInstances = []; + $this->checkedForAttributeBindings = []; } /** 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/ConcurrencyErrorDetector.php b/src/Illuminate/Database/ConcurrencyErrorDetector.php new file mode 100644 index 000000000000..0b388111bb65 --- /dev/null +++ b/src/Illuminate/Database/ConcurrencyErrorDetector.php @@ -0,0 +1,38 @@ +getCode() === 40001 || $e->getCode() === '40001')) { + return true; + } + + $message = $e->getMessage(); + + return Str::contains($message, [ + 'Deadlock found when trying to get lock', + 'deadlock detected', + 'The database file is locked', + 'database is locked', + 'database table is locked', + 'A table in the database is locked', + 'has been chosen as the deadlock victim', + 'Lock wait timeout exceeded; try restarting transaction', + 'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction', + ]); + } +} diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index a883e3edb22e..9910fe911b66 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -25,6 +25,8 @@ use PDOStatement; use RuntimeException; +use function Illuminate\Support\enum_value; + class Connection implements ConnectionInterface { use DetectsConcurrencyErrors, @@ -36,14 +38,14 @@ class Connection implements ConnectionInterface /** * The active PDO connection. * - * @var \PDO|\Closure + * @var \PDO|(\Closure(): \PDO) */ protected $pdo; /** * The active PDO connection used for reads. * - * @var \PDO|\Closure + * @var \PDO|(\Closure(): \PDO) */ protected $readPdo; @@ -78,7 +80,7 @@ class Connection implements ConnectionInterface /** * The reconnector instance for the connection. * - * @var callable + * @var (callable(\Illuminate\Database\Connection): mixed) */ protected $reconnector; @@ -148,7 +150,7 @@ class Connection implements ConnectionInterface /** * All of the queries run against the connection. * - * @var array + * @var array{query: string, bindings: array, time: float|null}[] */ protected $queryLog = []; @@ -169,7 +171,7 @@ class Connection implements ConnectionInterface /** * All of the registered query duration handlers. * - * @var array + * @var array{has_run: bool, handler: (callable(\Illuminate\Database\Connection, class-string<\Illuminate\Database\Events\QueryExecuted>): mixed)}[] */ protected $queryDurationHandlers = []; @@ -190,7 +192,7 @@ class Connection implements ConnectionInterface /** * All of the callbacks that should be invoked before a query is executed. * - * @var \Closure[] + * @var (\Closure(string, array, \Illuminate\Database\Connection): mixed)[] */ protected $beforeExecutingCallbacks = []; @@ -204,7 +206,7 @@ class Connection implements ConnectionInterface /** * Create a new database connection instance. * - * @param \PDO|\Closure $pdo + * @param \PDO|(\Closure(): \PDO) $pdo * @param string $database * @param string $tablePrefix * @param array $config @@ -307,13 +309,13 @@ public function getSchemaBuilder() /** * Begin a fluent query against a database table. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|string $table + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Contracts\Database\Query\Expression|\UnitEnum|string $table * @param string|null $as * @return \Illuminate\Database\Query\Builder */ public function table($table, $as = null) { - return $this->query()->from($table, $as); + return $this->query()->from(enum_value($table), $as); } /** @@ -636,8 +638,8 @@ public function threadCount() /** * Execute the given callback in "dry run" mode. * - * @param \Closure $callback - * @return array + * @param (\Closure(\Illuminate\Database\Connection): mixed) $callback + * @return array{query: string, bindings: array, time: float|null}[] */ public function pretend(Closure $callback) { @@ -679,8 +681,8 @@ public function withoutPretending(Closure $callback) /** * Execute the given callback in "dry run" mode. * - * @param \Closure $callback - * @return array + * @param (\Closure(): array{query: string, bindings: array, time: float|null}[]) $callback + * @return array{query: string, bindings: array, time: float|null}[] */ protected function withFreshQueryLog($callback) { @@ -874,7 +876,7 @@ protected function getElapsedTime($start) * Register a callback to be invoked when the connection queries for longer than a given amount of time. * * @param \DateTimeInterface|\Carbon\CarbonInterval|float|int $threshold - * @param callable $handler + * @param (callable(\Illuminate\Database\Connection, class-string<\Illuminate\Database\Events\QueryExecuted>): mixed) $handler * @return void */ public function whenQueryingForLongerThan($threshold, $handler) @@ -1305,7 +1307,7 @@ public function setReadPdo($pdo) /** * Set the reconnect instance on the connection. * - * @param callable $reconnector + * @param (callable(\Illuminate\Database\Connection): mixed) $reconnector * @return $this */ public function setReconnector(callable $reconnector) @@ -1504,7 +1506,7 @@ public function pretending() /** * Get the connection query log. * - * @return array + * @return array{query: string, bindings: array, time: float|null}[] */ public function getQueryLog() { diff --git a/src/Illuminate/Database/ConnectionInterface.php b/src/Illuminate/Database/ConnectionInterface.php index 288adb4206e3..b28c63f9c25b 100755 --- a/src/Illuminate/Database/ConnectionInterface.php +++ b/src/Illuminate/Database/ConnectionInterface.php @@ -9,7 +9,7 @@ interface ConnectionInterface /** * Begin a fluent query against a database table. * - * @param \Closure|\Illuminate\Database\Query\Builder|string $table + * @param \Closure|\Illuminate\Database\Query\Builder|\UnitEnum|string $table * @param string|null $as * @return \Illuminate\Database\Query\Builder */ diff --git a/src/Illuminate/Database/ConnectionResolverInterface.php b/src/Illuminate/Database/ConnectionResolverInterface.php index b31e5a792565..47161d37d69f 100755 --- a/src/Illuminate/Database/ConnectionResolverInterface.php +++ b/src/Illuminate/Database/ConnectionResolverInterface.php @@ -7,7 +7,7 @@ interface ConnectionResolverInterface /** * Get a database connection instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Database\ConnectionInterface */ public function connection($name = null); 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/Console/WipeCommand.php b/src/Illuminate/Database/Console/WipeCommand.php index 754c9eea8414..d638db41d0a4 100644 --- a/src/Illuminate/Database/Console/WipeCommand.php +++ b/src/Illuminate/Database/Console/WipeCommand.php @@ -57,6 +57,8 @@ public function handle() $this->components->info('Dropped all types successfully.'); } + $this->flushDatabaseConnection($database); + return 0; } @@ -99,6 +101,17 @@ protected function dropAllTypes($database) ->dropAllTypes(); } + /** + * Flush the given database connection. + * + * @param string $database + * @return void + */ + protected function flushDatabaseConnection($database) + { + $this->laravel['db']->connection($database)->disconnect(); + } + /** * Get the console command options. * diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index 4d3aafc83fe3..c5529d69cec8 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -13,6 +13,8 @@ use PDO; use RuntimeException; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Database\Connection */ @@ -85,12 +87,12 @@ public function __construct($app, ConnectionFactory $factory) /** * Get a database connection instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Database\Connection */ public function connection($name = null) { - $name = $name ?: $this->getDefaultConnection(); + $name = enum_value($name) ?: $this->getDefaultConnection(); [$database, $type] = $this->parseConnectionName($name); @@ -116,9 +118,7 @@ public function connection($name = null) */ public function build(array $config) { - if (! isset($config['name'])) { - $config['name'] = static::calculateDynamicConnectionName($config); - } + $config['name'] ??= static::calculateDynamicConnectionName($config); $this->dynamicConnectionConfigurations[$config['name']] = $config; @@ -354,9 +354,11 @@ public function usingConnection($name, callable $callback) $this->setDefaultConnection($name); - return tap($callback(), function () use ($previousName) { + try { + return $callback(); + } finally { $this->setDefaultConnection($previousName); - }); + } } /** diff --git a/src/Illuminate/Database/DatabaseServiceProvider.php b/src/Illuminate/Database/DatabaseServiceProvider.php index e322cad4c71b..794090b34776 100755 --- a/src/Illuminate/Database/DatabaseServiceProvider.php +++ b/src/Illuminate/Database/DatabaseServiceProvider.php @@ -4,6 +4,8 @@ use Faker\Factory as FakerFactory; use Faker\Generator as FakerGenerator; +use Illuminate\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract; +use Illuminate\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract; use Illuminate\Contracts\Queue\EntityResolver; use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\Eloquent\Model; @@ -77,6 +79,14 @@ protected function registerConnectionServices() $this->app->singleton('db.transactions', function ($app) { return new DatabaseTransactionsManager; }); + + $this->app->singleton(ConcurrencyErrorDetectorContract::class, function ($app) { + return new ConcurrencyErrorDetector; + }); + + $this->app->singleton(LostConnectionDetectorContract::class, function ($app) { + return new LostConnectionDetector; + }); } /** diff --git a/src/Illuminate/Database/DetectsConcurrencyErrors.php b/src/Illuminate/Database/DetectsConcurrencyErrors.php index c6c66f43563e..34659d64cd98 100644 --- a/src/Illuminate/Database/DetectsConcurrencyErrors.php +++ b/src/Illuminate/Database/DetectsConcurrencyErrors.php @@ -2,8 +2,8 @@ namespace Illuminate\Database; -use Illuminate\Support\Str; -use PDOException; +use Illuminate\Container\Container; +use Illuminate\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract; use Throwable; trait DetectsConcurrencyErrors @@ -16,22 +16,12 @@ trait DetectsConcurrencyErrors */ protected function causedByConcurrencyError(Throwable $e) { - if ($e instanceof PDOException && ($e->getCode() === 40001 || $e->getCode() === '40001')) { - return true; - } + $container = Container::getInstance(); - $message = $e->getMessage(); + $detector = $container->bound(ConcurrencyErrorDetectorContract::class) + ? $container[ConcurrencyErrorDetectorContract::class] + : new ConcurrencyErrorDetector(); - return Str::contains($message, [ - 'Deadlock found when trying to get lock', - 'deadlock detected', - 'The database file is locked', - 'database is locked', - 'database table is locked', - 'A table in the database is locked', - 'has been chosen as the deadlock victim', - 'Lock wait timeout exceeded; try restarting transaction', - 'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction', - ]); + return $detector->causedByConcurrencyError($e); } } diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index 72b5a043288e..ba649afe2aab 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -2,7 +2,8 @@ namespace Illuminate\Database; -use Illuminate\Support\Str; +use Illuminate\Container\Container; +use Illuminate\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract; use Throwable; trait DetectsLostConnections @@ -15,73 +16,12 @@ trait DetectsLostConnections */ protected function causedByLostConnection(Throwable $e) { - $message = $e->getMessage(); + $container = Container::getInstance(); - return Str::contains($message, [ - 'server has gone away', - 'Server has gone away', - 'no connection to the server', - 'Lost connection', - 'is dead or not enabled', - 'Error while sending', - 'decryption failed or bad record mac', - 'server closed the connection unexpectedly', - 'SSL connection has been closed unexpectedly', - 'Error writing data to the connection', - 'Resource deadlock avoided', - 'Transaction() on null', - 'child connection forced to terminate due to client_idle_limit', - 'query_wait_timeout', - 'reset by peer', - 'Physical connection is not usable', - 'TCP Provider: Error code 0x68', - 'ORA-03114', - 'Packets out of order. Expected', - 'Adaptive Server connection failed', - 'Communication link failure', - 'connection is no longer usable', - 'Login timeout expired', - 'SQLSTATE[HY000] [2002] Connection refused', - 'running with the --read-only option so it cannot execute this statement', - 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', - 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', - 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', - 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for', - 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', - 'SQLSTATE[HY000] [2002] Connection timed out', - 'SSL: Connection timed out', - 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', - 'Temporary failure in name resolution', - 'SQLSTATE[08S01]: Communication link failure', - 'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host', - 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host', - 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.', - 'SQLSTATE[08006] [7] could not translate host name', - 'TCP Provider: Error code 0x274C', - 'SQLSTATE[HY000] [2002] No such file or directory', - 'SSL: Operation timed out', - 'Reason: Server is in script upgrade mode. Only administrator can connect at this time.', - 'Unknown $curl_error_code: 77', - 'SSL: Handshake timed out', - 'SSL error: sslv3 alert unexpected message', - 'unrecognized SSL error code:', - 'SQLSTATE[HY000] [1045] Access denied for user', - 'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it', - 'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond', - 'SQLSTATE[HY000] [2002] Network is unreachable', - 'SQLSTATE[HY000] [2002] The requested address is not valid in its context', - 'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network', - 'SQLSTATE[HY000] [2002] Operation now in progress', - 'SQLSTATE[HY000] [2002] Operation in progress', - 'SQLSTATE[HY000]: General error: 3989', - 'went away', - 'No such file or directory', - 'server is shutting down', - 'failed to connect to', - 'Channel connection is closed', - 'Connection lost', - 'Broken pipe', - 'SQLSTATE[25006]: Read only sql transaction: 7', - ]); + $detector = $container->bound(LostConnectionDetectorContract::class) + ? $container[LostConnectionDetectorContract::class] + : new LostConnectionDetector(); + + return $detector->causedByLostConnection($e); } } diff --git a/src/Illuminate/Database/Eloquent/Attributes/Boot.php b/src/Illuminate/Database/Eloquent/Attributes/Boot.php new file mode 100644 index 000000000000..f57da7af9450 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/Boot.php @@ -0,0 +1,11 @@ + $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..b53f26a2c699 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', @@ -309,6 +310,21 @@ public function whereKeyNot($id) return $this->where($this->model->getQualifiedKeyName(), '!=', $id); } + /** + * Exclude the given models from the query results. + * + * @param iterable|mixed $models + * @return static + */ + public function except($models) + { + return $this->whereKeyNot( + $models instanceof Model + ? $models->getKey() + : Collection::wrap($models)->modelKeys() + ); + } + /** * Add a basic where clause to the query. * @@ -1118,7 +1134,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/AsEnumArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php index 273089b2318a..061dcbf57e96 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php @@ -68,9 +68,9 @@ public function set($model, $key, $value, $attributes) public function serialize($model, string $key, $value, array $attributes) { - return (new Collection($value->getArrayCopy()))->map(function ($enum) { - return $this->getStorableEnumValue($enum); - })->toArray(); + return (new Collection($value->getArrayCopy())) + ->map(fn ($enum) => $this->getStorableEnumValue($enum)) + ->toArray(); } protected function getStorableEnumValue($enum) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php index 044c4578652c..fa7116a0d0ed 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php @@ -64,9 +64,9 @@ public function set($model, $key, $value, $attributes) public function serialize($model, string $key, $value, array $attributes) { - return (new Collection($value))->map(function ($enum) { - return $this->getStorableEnumValue($enum); - })->toArray(); + return (new Collection($value)) + ->map(fn ($enum) => $this->getStorableEnumValue($enum)) + ->toArray(); } protected function getStorableEnumValue($enum) 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..3f267914902c 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; @@ -705,8 +706,8 @@ public function pad($size, $value) * Partition the collection into two arrays using the given callback or key. * * @param (callable(TModel, TKey): bool)|TModel|string $key - * @param TModel|string|null $operator - * @param TModel|null $value + * @param mixed $operator + * @param mixed $value * @return \Illuminate\Support\Collection, static> */ public function partition($key, $operator = null, $value = null) @@ -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..e09580db48df 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -14,7 +14,7 @@ trait GuardsAttributes /** * The attributes that aren't mass assignable. * - * @var array|bool + * @var array */ protected $guarded = ['*']; @@ -28,7 +28,7 @@ trait GuardsAttributes /** * The actual columns that exist on the database and can be guarded. * - * @var array + * @var array> */ protected static $guardableColumns = []; @@ -75,7 +75,7 @@ public function mergeFillable(array $fillable) */ public function getGuarded() { - return $this->guarded === false + return self::$unguarded === true ? [] : $this->guarded; } @@ -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/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index b7955bd111a9..805cca3b21de 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -89,6 +89,8 @@ protected function hasNested($relations, $operator = '>=', $count = 1, $boolean { $relations = explode('.', $relations); + $initialRelations = [...$relations]; + $doesntHave = $operator === '<' && $count === 1; if ($doesntHave) { @@ -96,7 +98,14 @@ protected function hasNested($relations, $operator = '>=', $count = 1, $boolean $count = 1; } - $closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback) { + $closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback, $initialRelations) { + // If the same closure is called multiple times, reset the relation array to loop through them again... + if ($count === 1 && empty($relations)) { + $relations = [...$initialRelations]; + + array_shift($relations); + } + // In order to nest "has", we need to add count relation constraints on the // callback Closure. We'll do this by simply passing the Closure its own // reference to itself so it calls itself recursively on each segment. diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index a52d840f421e..f3a4c26f2b39 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; @@ -92,6 +91,13 @@ abstract class Factory */ protected $expandRelationships = true; + /** + * The relationships that should not be automatically created. + * + * @var array + */ + protected $excludeRelationships = []; + /** * The name of the database connection that will be used to create the models. * @@ -134,6 +140,13 @@ abstract class Factory */ protected static $factoryNameResolver; + /** + * Whether to expand relationships by default. + * + * @var bool + */ + protected static $expandRelationshipsByDefault = true; + /** * Create a new factory instance. * @@ -145,7 +158,8 @@ abstract class Factory * @param \Illuminate\Support\Collection|null $afterCreating * @param string|null $connection * @param \Illuminate\Support\Collection|null $recycle - * @param bool $expandRelationships + * @param bool|null $expandRelationships + * @param array $excludeRelationships */ public function __construct( $count = null, @@ -156,7 +170,8 @@ public function __construct( ?Collection $afterCreating = null, $connection = null, ?Collection $recycle = null, - bool $expandRelationships = true + ?bool $expandRelationships = null, + array $excludeRelationships = [], ) { $this->count = $count; $this->states = $states ?? new Collection; @@ -167,7 +182,8 @@ public function __construct( $this->connection = $connection; $this->recycle = $recycle ?? new Collection; $this->faker = $this->withFaker(); - $this->expandRelationships = $expandRelationships; + $this->expandRelationships = $expandRelationships ?? self::$expandRelationshipsByDefault; + $this->excludeRelationships = $excludeRelationships; } /** @@ -395,27 +411,37 @@ public function makeOne($attributes = []) */ public function make($attributes = [], ?Model $parent = null) { - if (! empty($attributes)) { - return $this->state($attributes)->make([], $parent); - } + $autoEagerLoadingEnabled = Model::isAutomaticallyEagerLoadingRelationships(); - if ($this->count === null) { - return tap($this->makeInstance($parent), function ($instance) { - $this->callAfterMaking(new Collection([$instance])); - }); + if ($autoEagerLoadingEnabled) { + Model::automaticallyEagerLoadRelationships(false); } - if ($this->count < 1) { - return $this->newModel()->newCollection(); - } + try { + if (! empty($attributes)) { + return $this->state($attributes)->make([], $parent); + } + + if ($this->count === null) { + return tap($this->makeInstance($parent), function ($instance) { + $this->callAfterMaking(new Collection([$instance])); + }); + } - $instances = $this->newModel()->newCollection(array_map(function () use ($parent) { - return $this->makeInstance($parent); - }, range(1, $this->count))); + if ($this->count < 1) { + return $this->newModel()->newCollection(); + } + + $instances = $this->newModel()->newCollection(array_map(function () use ($parent) { + return $this->makeInstance($parent); + }, range(1, $this->count))); - $this->callAfterMaking($instances); + $this->callAfterMaking($instances); - return $instances; + return $instances; + } finally { + Model::automaticallyEagerLoadRelationships($autoEagerLoadingEnabled); + } } /** @@ -489,9 +515,12 @@ protected function parentResolvers() protected function expandAttributes(array $definition) { return (new Collection($definition)) - ->map($evaluateRelations = function ($attribute) { + ->map($evaluateRelations = function ($attribute, $key) { if (! $this->expandRelationships && $attribute instanceof self) { $attribute = null; + } elseif ($attribute instanceof self && + array_intersect([$attribute->modelName(), $key], $this->excludeRelationships)) { + $attribute = null; } elseif ($attribute instanceof self) { $attribute = $this->getRandomRecycledModel($attribute->modelName())?->getKey() ?? $attribute->recycle($this->recycle)->create()->getKey(); @@ -506,7 +535,7 @@ protected function expandAttributes(array $definition) $attribute = $attribute($definition); } - $attribute = $evaluateRelations($attribute); + $attribute = $evaluateRelations($attribute, $key); $definition[$key] = $attribute; @@ -518,7 +547,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 +562,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) @@ -758,11 +787,12 @@ public function count(?int $count) /** * Indicate that related parent models should not be created. * + * @param array> $parents * @return static */ - public function withoutParents() + public function withoutParents($parents = []) { - return $this->newInstance(['expandRelationships' => false]); + return $this->newInstance(! $parents ? ['expandRelationships' => false] : ['excludeRelationships' => $parents]); } /** @@ -804,6 +834,7 @@ protected function newInstance(array $arguments = []) 'connection' => $this->connection, 'recycle' => $this->recycle, 'expandRelationships' => $this->expandRelationships, + 'excludeRelationships' => $this->excludeRelationships, ], $arguments))); } @@ -896,6 +927,26 @@ public static function guessFactoryNamesUsing(callable $callback) static::$factoryNameResolver = $callback; } + /** + * Specify that relationships should create parent relationships by default. + * + * @return void + */ + public static function expandRelationshipsByDefault() + { + static::$expandRelationshipsByDefault = true; + } + + /** + * Specify that relationships should not create parent relationships by default. + * + * @return void + */ + public static function dontExpandRelationshipsByDefault() + { + static::$expandRelationshipsByDefault = false; + } + /** * Get a new Faker instance. * @@ -956,6 +1007,7 @@ public static function flushState() static::$modelNameResolvers = []; static::$factoryNameResolver = null; static::$namespace = 'Database\\Factories\\'; + static::$expandRelationshipsByDefault = true; } /** @@ -971,7 +1023,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..94973c365318 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -12,7 +12,10 @@ use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\ConnectionResolverInterface as Resolver; +use Illuminate\Database\Eloquent\Attributes\Boot; +use Illuminate\Database\Eloquent\Attributes\Initialize; 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 +29,7 @@ use JsonException; use JsonSerializable; use LogicException; +use ReflectionClass; use ReflectionMethod; use Stringable; @@ -248,6 +252,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 +290,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 = []) { @@ -337,23 +362,28 @@ protected static function bootTraits() static::$traitInitializers[$class] = []; - foreach (class_uses_recursive($class) as $trait) { - $method = 'boot'.class_basename($trait); + $uses = class_uses_recursive($class); - if (method_exists($class, $method) && ! in_array($method, $booted)) { - forward_static_call([$class, $method]); + $conventionalBootMethods = array_map(static fn ($trait) => 'boot'.class_basename($trait), $uses); + $conventionalInitMethods = array_map(static fn ($trait) => 'initialize'.class_basename($trait), $uses); - $booted[] = $method; - } + foreach ((new ReflectionClass($class))->getMethods() as $method) { + if (! in_array($method->getName(), $booted) && + $method->isStatic() && + (in_array($method->getName(), $conventionalBootMethods) || + $method->getAttributes(Boot::class) !== [])) { + $method->invoke(null); - if (method_exists($class, $method = 'initialize'.class_basename($trait))) { - static::$traitInitializers[$class][] = $method; + $booted[] = $method->getName(); + } - static::$traitInitializers[$class] = array_unique( - static::$traitInitializers[$class] - ); + if (in_array($method->getName(), $conventionalInitMethods) || + $method->getAttributes(Initialize::class) !== []) { + static::$traitInitializers[$class][] = $method->getName(); } } + + static::$traitInitializers[$class] = array_unique(static::$traitInitializers[$class]); } /** @@ -568,7 +598,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 +648,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 +687,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 +716,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 */ @@ -714,11 +744,7 @@ public static function on($connection = null) // First we will just create a fresh instance of this model, and then we can set the // connection on the model so that it is used for the queries we execute, as well // as being set on every relation we retrieve without a custom connection name. - $instance = new static; - - $instance->setConnection($connection); - - return $instance->newQuery(); + return (new static)->setConnection($connection)->newQuery(); } /** @@ -1049,8 +1075,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 +1091,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 +1109,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 +1426,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 +1677,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 +1715,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 +2313,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. * @@ -2399,7 +2470,12 @@ public function offsetSet($offset, $value): void */ public function offsetUnset($offset): void { - unset($this->attributes[$offset], $this->relations[$offset], $this->attributeCastCache[$offset]); + unset( + $this->attributes[$offset], + $this->relations[$offset], + $this->attributeCastCache[$offset], + $this->classCastCache[$offset] + ); } /** 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/LostConnectionDetector.php b/src/Illuminate/Database/LostConnectionDetector.php new file mode 100644 index 000000000000..0e0ce0d50d4b --- /dev/null +++ b/src/Illuminate/Database/LostConnectionDetector.php @@ -0,0 +1,88 @@ +getMessage(); + + return Str::contains($message, [ + 'server has gone away', + 'Server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'Transaction() on null', + 'child connection forced to terminate due to client_idle_limit', + 'query_wait_timeout', + 'reset by peer', + 'Physical connection is not usable', + 'TCP Provider: Error code 0x68', + 'ORA-03114', + 'Packets out of order. Expected', + 'Adaptive Server connection failed', + 'Communication link failure', + 'connection is no longer usable', + 'Login timeout expired', + 'SQLSTATE[HY000] [2002] Connection refused', + 'running with the --read-only option so it cannot execute this statement', + 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', + 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for', + 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', + 'SQLSTATE[HY000] [2002] Connection timed out', + 'SSL: Connection timed out', + 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', + 'Temporary failure in name resolution', + 'SQLSTATE[08S01]: Communication link failure', + 'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host', + 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host', + 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.', + 'SQLSTATE[08006] [7] could not translate host name', + 'TCP Provider: Error code 0x274C', + 'SQLSTATE[HY000] [2002] No such file or directory', + 'SSL: Operation timed out', + 'Reason: Server is in script upgrade mode. Only administrator can connect at this time.', + 'Unknown $curl_error_code: 77', + 'SSL: Handshake timed out', + 'SSL error: sslv3 alert unexpected message', + 'unrecognized SSL error code:', + 'SQLSTATE[HY000] [1045] Access denied for user', + 'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it', + 'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond', + 'SQLSTATE[HY000] [2002] Network is unreachable', + 'SQLSTATE[HY000] [2002] The requested address is not valid in its context', + 'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network', + 'SQLSTATE[HY000] [2002] Operation now in progress', + 'SQLSTATE[HY000] [2002] Operation in progress', + 'SQLSTATE[HY000]: General error: 3989', + 'went away', + 'No such file or directory', + 'server is shutting down', + 'failed to connect to', + 'Channel connection is closed', + 'Connection lost', + 'Broken pipe', + 'SQLSTATE[25006]: Read only sql transaction: 7', + ]); + } +} 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/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index d50c6d6b2d0d..1ad82c2e035d 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -662,7 +662,11 @@ public function usingConnection($name, callable $callback) $this->setConnection($name); - return tap($callback(), fn () => $this->setConnection($previousConnection)); + try { + return $callback(); + } finally { + $this->setConnection($previousConnection); + } } /** diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index d2b97d5d121e..7c585a169533 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -1489,6 +1489,63 @@ public function orWhereNotBetweenColumns($column, array $values) return $this->whereNotBetweenColumns($column, $values, 'or'); } + /** + * Add a where between columns statement using a value to the query. + * + * @param mixed $value + * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns + * @param string $boolean + * @param bool $not + * @return $this + */ + public function whereValueBetween($value, array $columns, $boolean = 'and', $not = false) + { + $type = 'valueBetween'; + + $this->wheres[] = compact('type', 'value', 'columns', 'boolean', 'not'); + + $this->addBinding($value, 'where'); + + return $this; + } + + /** + * Add an or where between columns statement using a value to the query. + * + * @param mixed $value + * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns + * @return $this + */ + public function orWhereValueBetween($value, array $columns) + { + return $this->whereValueBetween($value, $columns, 'or'); + } + + /** + * Add a where not between columns statement using a value to the query. + * + * @param mixed $value + * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns + * @param string $boolean + * @return $this + */ + public function whereValueNotBetween($value, array $columns, $boolean = 'and') + { + return $this->whereValueBetween($value, $columns, $boolean, true); + } + + /** + * Add an or where not between columns statement using a value to the query. + * + * @param mixed $value + * @param array{\Illuminate\Contracts\Database\Query\Expression|string, \Illuminate\Contracts\Database\Query\Expression|string} $columns + * @return $this + */ + public function orWhereValueNotBetween($value, array $columns) + { + return $this->whereValueNotBetween($value, $columns, 'or'); + } + /** * Add an "or where not null" clause to the query. * @@ -2856,6 +2913,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 +3319,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 +3608,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 +3810,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 +3892,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 +3966,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 +4010,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 +4028,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 +4052,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 +4070,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/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index ea1c44e00866..27effd7c4a34 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -455,6 +455,24 @@ protected function whereBetweenColumns(Builder $query, $where) return $this->wrap($where['column']).' '.$between.' '.$min.' and '.$max; } + /** + * Compile a "value between" where clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereValueBetween(Builder $query, $where) + { + $between = $where['not'] ? 'not between' : 'between'; + + $min = $this->wrap(is_array($where['columns']) ? reset($where['columns']) : $where['columns'][0]); + + $max = $this->wrap(is_array($where['columns']) ? end($where['columns']) : $where['columns'][1]); + + return $this->parameter($where['value']).' '.$between.' '.$min.' and '.$max; + } + /** * Compile a "where date" clause. * @@ -1177,9 +1195,9 @@ public function compileInsert(Builder $query, array $values) // We need to build a list of parameter place-holders of values that are bound // to the query. Each insert should have the exact same number of parameter // bindings so we will loop through the record and parameterize them all. - $parameters = (new Collection($values))->map(function ($record) { - return '('.$this->parameterize($record).')'; - })->implode(', '); + $parameters = (new Collection($values)) + ->map(fn ($record) => '('.$this->parameterize($record).')') + ->implode(', '); return "insert into $table ($columns) values $parameters"; } @@ -1276,9 +1294,9 @@ public function compileUpdate(Builder $query, array $values) */ protected function compileUpdateColumns(Builder $query, array $values) { - return (new Collection($values))->map(function ($value, $key) { - return $this->wrap($key).' = '.$this->parameter($value); - })->implode(', '); + return (new Collection($values)) + ->map(fn ($value, $key) => $this->wrap($key).' = '.$this->parameter($value)) + ->implode(', '); } /** diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index 9207fe54565f..615852e2ae7d 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -167,9 +167,9 @@ public function whereFullText(Builder $query, $where) $language = 'english'; } - $columns = (new Collection($where['columns']))->map(function ($column) use ($language) { - return "to_tsvector('{$language}', {$this->wrap($column)})"; - })->implode(' || '); + $columns = (new Collection($where['columns'])) + ->map(fn ($column) => "to_tsvector('{$language}', {$this->wrap($column)})") + ->implode(' || '); $mode = 'plainto_tsquery'; diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 9fb8d8a31589..a70571d2ceb9 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -289,15 +289,17 @@ protected function compileUpdateColumns(Builder $query, array $values) { $jsonGroups = $this->groupJsonColumnsForUpdate($values); - return (new Collection($values))->reject(function ($value, $key) { - return $this->isJsonSelector($key); - })->merge($jsonGroups)->map(function ($value, $key) use ($jsonGroups) { - $column = last(explode('.', $key)); + return (new Collection($values)) + ->reject(fn ($value, $key) => $this->isJsonSelector($key)) + ->merge($jsonGroups) + ->map(function ($value, $key) use ($jsonGroups) { + $column = last(explode('.', $key)); - $value = isset($jsonGroups[$key]) ? $this->compileJsonPatch($column, $value) : $this->parameter($value); + $value = isset($jsonGroups[$key]) ? $this->compileJsonPatch($column, $value) : $this->parameter($value); - return $this->wrap($column).' = '.$value; - })->implode(', '); + return $this->wrap($column).' = '.$value; + }) + ->implode(', '); } /** diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index c5e91c50e1bf..6426abbfde0f 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -419,15 +419,15 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar $sql = 'merge '.$this->wrapTable($query->from).' '; - $parameters = (new Collection($values))->map(function ($record) { - return '('.$this->parameterize($record).')'; - })->implode(', '); + $parameters = (new Collection($values)) + ->map(fn ($record) => '('.$this->parameterize($record).')') + ->implode(', '); $sql .= 'using (values '.$parameters.') '.$this->wrapTable('laravel_source').' ('.$columns.') '; - $on = (new Collection($uniqueBy))->map(function ($column) use ($query) { - return $this->wrap('laravel_source.'.$column).' = '.$this->wrap($query->from.'.'.$column); - })->implode(' and '); + $on = (new Collection($uniqueBy)) + ->map(fn ($column) => $this->wrap('laravel_source.'.$column).' = '.$this->wrap($query->from.'.'.$column)) + ->implode(' and '); $sql .= 'on '.$on.' '; diff --git a/src/Illuminate/Database/Query/JoinClause.php b/src/Illuminate/Database/Query/JoinClause.php index a9168087b254..d5733f35504b 100755 --- a/src/Illuminate/Database/Query/JoinClause.php +++ b/src/Illuminate/Database/Query/JoinClause.php @@ -16,7 +16,7 @@ class JoinClause extends Builder /** * The table the join clause is joining to. * - * @var string + * @var \Illuminate\Contracts\Database\Query\Expression|string */ public $table; diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index b7687e839f34..0f83b5f03a8f 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -13,6 +13,8 @@ use Illuminate\Support\Fluent; use Illuminate\Support\Traits\Macroable; +use function Illuminate\Support\enum_value; + class Blueprint { use Macroable; @@ -179,9 +181,8 @@ protected function ensureCommandsAreValid() */ protected function commandsNamed(array $names) { - return (new Collection($this->commands))->filter(function ($command) use ($names) { - return in_array($command->name, $names); - }); + return (new Collection($this->commands)) + ->filter(fn ($command) => in_array($command->name, $names)); } /** @@ -316,9 +317,8 @@ public function addAlterCommands() */ public function creating() { - return (new Collection($this->commands))->contains(function ($command) { - return ! $command instanceof ColumnDefinition && $command->name === 'create'; - }); + return (new Collection($this->commands)) + ->contains(fn ($command) => ! $command instanceof ColumnDefinition && $command->name === 'create'); } /** @@ -686,11 +686,12 @@ public function fullText($columns, $name = null, $algorithm = null) * * @param string|array $columns * @param string|null $name + * @param string|null $operatorClass * @return \Illuminate\Database\Schema\IndexDefinition */ - public function spatialIndex($columns, $name = null) + public function spatialIndex($columns, $name = null, $operatorClass = null) { - return $this->indexCommand('spatialIndex', $columns, $name); + return $this->indexCommand('spatialIndex', $columns, $name, null, $operatorClass); } /** @@ -1103,6 +1104,8 @@ public function boolean($column) */ public function enum($column, array $allowed) { + $allowed = array_map(fn ($value) => enum_value($value), $allowed); + return $this->addColumn('enum', $column, compact('allowed')); } @@ -1239,13 +1242,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 +1258,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(), + ]); } /** @@ -1640,15 +1646,16 @@ public function comment($comment) } /** - * Add a new index command to the blueprint. + * Create a new index command on the blueprint. * * @param string $type * @param string|array $columns * @param string $index * @param string|null $algorithm + * @param string|null $operatorClass * @return \Illuminate\Support\Fluent */ - protected function indexCommand($type, $columns, $index, $algorithm = null) + protected function indexCommand($type, $columns, $index, $algorithm = null, $operatorClass = null) { $columns = (array) $columns; @@ -1658,7 +1665,7 @@ protected function indexCommand($type, $columns, $index, $algorithm = null) $index = $index ?: $this->createIndexName($type, $columns); return $this->addCommand( - $type, compact('index', 'columns', 'algorithm') + $type, compact('index', 'columns', 'algorithm', 'operatorClass') ); } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index c22019536e7c..cf3018f89699 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -30,7 +30,7 @@ class Builder /** * The Blueprint resolver callback. * - * @var \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint|null + * @var \Closure(\Illuminate\Database\Connection, string, \Closure|null): \Illuminate\Database\Schema\Blueprint */ protected $resolver; @@ -698,7 +698,7 @@ public function getConnection() /** * Set the Schema Blueprint resolver callback. * - * @param \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint|null $resolver + * @param \Closure(\Illuminate\Database\Connection, string, \Closure|null): \Illuminate\Database\Schema\Blueprint $resolver * @return void */ public function blueprintResolver(Closure $resolver) 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/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 7e1e6a1d2fa8..708e75058d1f 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -403,9 +403,46 @@ public function compileSpatialIndex(Blueprint $blueprint, Fluent $command) { $command->algorithm = 'gist'; + if (! is_null($command->operatorClass)) { + return $this->compileIndexWithOperatorClass($blueprint, $command); + } + return $this->compileIndex($blueprint, $command); } + /** + * Compile a spatial index with operator class key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + protected function compileIndexWithOperatorClass(Blueprint $blueprint, Fluent $command) + { + $columns = $this->columnizeWithOperatorClass($command->columns, $command->operatorClass); + + return sprintf('create index %s on %s%s (%s)', + $this->wrap($command->index), + $this->wrapTable($blueprint), + $command->algorithm ? ' using '.$command->algorithm : '', + $columns + ); + } + + /** + * Convert an array of column names to a delimited string with operator class. + * + * @param array $columns + * @param string $operatorClass + * @return string + */ + protected function columnizeWithOperatorClass(array $columns, $operatorClass) + { + return implode(', ', array_map(function ($column) use ($operatorClass) { + return $this->wrap($column).' '.$operatorClass; + }, $columns)); + } + /** * Compile a foreign key command. * 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/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index c49a49d30ad7..08140d9223f3 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -69,6 +69,27 @@ class Dispatcher implements DispatcherContract */ protected $transactionManagerResolver; + /** + * The currently deferred events. + * + * @var array + */ + protected $deferredEvents = []; + + /** + * Indicates if events should be deferred. + * + * @var bool + */ + protected $deferringEvents = false; + + /** + * The specific events to defer (null means defer all events). + * + * @var array|null + */ + protected $eventsToDefer = null; + /** * Create a new event dispatcher instance. * @@ -252,6 +273,12 @@ public function dispatch($event, $payload = [], $halt = false) ...$this->parseEventAndPayload($event, $payload), ]; + if ($this->shouldDeferEvent($event)) { + $this->deferredEvents[] = func_get_args(); + + return null; + } + // If the event is not intended to be dispatched unless the current database // transaction is successful, we'll register a callback which will handle // dispatching this event on the next successful DB transaction commit. @@ -768,6 +795,51 @@ public function setTransactionManagerResolver(callable $resolver) return $this; } + /** + * Execute the given callback while deferring events, then dispatch all deferred events. + * + * @param callable $callback + * @param array|null $events + * @return mixed + */ + public function defer(callable $callback, ?array $events = null) + { + $wasDeferring = $this->deferringEvents; + $previousDeferredEvents = $this->deferredEvents; + $previousEventsToDefer = $this->eventsToDefer; + + $this->deferringEvents = true; + $this->deferredEvents = []; + $this->eventsToDefer = $events; + + try { + $result = $callback(); + + $this->deferringEvents = false; + + foreach ($this->deferredEvents as $args) { + $this->dispatch(...$args); + } + + return $result; + } finally { + $this->deferringEvents = $wasDeferring; + $this->deferredEvents = $previousDeferredEvents; + $this->eventsToDefer = $previousEventsToDefer; + } + } + + /** + * Determine if the given event should be deferred. + * + * @param string $event + * @return bool + */ + protected function shouldDeferEvent(string $event) + { + return $this->deferringEvents && ($this->eventsToDefer === null || in_array($event, $this->eventsToDefer)); + } + /** * Gets the raw, unprepared listeners. * diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 50ce21f3671d..588f8c66f973 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -159,7 +159,7 @@ public function assertCount($path, $count, $recursive = false) $actual = count($this->files($path, $recursive)); PHPUnit::assertEquals( - $actual, $count, "Expected [{$count}] files at [{$path}], but found [{$actual}]." + $count, $actual, "Expected [{$count}] files at [{$path}], but found [{$actual}]." ); return $this; diff --git a/src/Illuminate/Filesystem/FilesystemManager.php b/src/Illuminate/Filesystem/FilesystemManager.php index db6f82ddca0a..53372b53f40e 100644 --- a/src/Illuminate/Filesystem/FilesystemManager.php +++ b/src/Illuminate/Filesystem/FilesystemManager.php @@ -21,6 +21,8 @@ use League\Flysystem\UnixVisibility\PortableVisibilityConverter; use League\Flysystem\Visibility; +use function Illuminate\Support\enum_value; + /** * @mixin \Illuminate\Contracts\Filesystem\Filesystem * @mixin \Illuminate\Filesystem\FilesystemAdapter @@ -72,12 +74,12 @@ public function drive($name = null) /** * Get a filesystem instance. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Contracts\Filesystem\Filesystem */ public function disk($name = null) { - $name = $name ?: $this->getDefaultDriver(); + $name = enum_value($name) ?: $this->getDefaultDriver(); return $this->disks[$name] = $this->get($name); } diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 243144794d59..64b04d909bdd 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.1'; + const VERSION = '12.21.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php b/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php index 2fa429f83034..4c5f00e9a2c0 100644 --- a/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php +++ b/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php @@ -5,6 +5,7 @@ use Illuminate\Config\Repository; use Illuminate\Contracts\Config\Repository as RepositoryContract; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Support\Collection; use SplFileInfo; use Symfony\Component\Finder\Finder; @@ -43,6 +44,8 @@ public function bootstrap(Application $app) // the environment in a web context where an "--env" switch is not present. $app->detectEnvironment(fn () => $config->get('app.env', 'production')); + $app->resolveEnvironmentUsing($app->environment(...)); + date_default_timezone_set($config->get('app.timezone', 'UTC')); mb_internal_encoding('UTF-8'); @@ -69,7 +72,7 @@ protected function loadConfigurationFiles(Application $app, RepositoryContract $ ? $this->getBaseConfiguration() : []; - foreach (array_diff(array_keys($base), array_keys($files)) as $name => $config) { + foreach ((new Collection($base))->diffKeys($files) as $name => $config) { $repository->set($name, $config); } diff --git a/src/Illuminate/Foundation/Bus/PendingChain.php b/src/Illuminate/Foundation/Bus/PendingChain.php index bcb381e51617..b2976e756b1b 100644 --- a/src/Illuminate/Foundation/Bus/PendingChain.php +++ b/src/Illuminate/Foundation/Bus/PendingChain.php @@ -3,8 +3,10 @@ namespace Illuminate\Foundation\Bus; use Closure; +use Illuminate\Bus\ChainedBatch; use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Queue\CallQueuedClosure; +use Illuminate\Support\Collection; use Illuminate\Support\Traits\Conditionable; use Laravel\SerializableClosure\SerializableClosure; @@ -94,6 +96,50 @@ public function onQueue($queue) return $this; } + /** + * Prepend a job to the chain. + * + * @param mixed $job + * @return $this + */ + public function prepend($job) + { + $jobs = ChainedBatch::prepareNestedBatches( + Collection::wrap($job) + ); + + if ($this->job) { + array_unshift($this->chain, $this->job); + } + + $this->job = $jobs->shift(); + + array_unshift($this->chain, ...$jobs->toArray()); + + return $this; + } + + /** + * Append a job to the chain. + * + * @param mixed $job + * @return $this + */ + public function append($job) + { + $jobs = ChainedBatch::prepareNestedBatches( + Collection::wrap($job) + ); + + if (! $this->job) { + $this->job = $jobs->shift(); + } + + array_push($this->chain, ...$jobs->toArray()); + + return $this; + } + /** * Set the desired delay in seconds for the chain. * diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index e0ffdd534495..e386d8273c8e 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -417,6 +417,25 @@ public function withSingletons(array $singletons) }); } + /** + * Register an array of scoped singleton container bindings to be bound when the application is booting. + * + * @param array $scopedSingletons + * @return $this + */ + public function withScopedSingletons(array $scopedSingletons) + { + return $this->registered(function ($app) use ($scopedSingletons) { + foreach ($scopedSingletons as $abstract => $concrete) { + if (is_string($abstract)) { + $app->scoped($abstract, $concrete); + } else { + $app->scoped($concrete); + } + } + }); + } + /** * Register a callback to be invoked when the application's service providers are registered. * diff --git a/src/Illuminate/Foundation/Configuration/Exceptions.php b/src/Illuminate/Foundation/Configuration/Exceptions.php index 1072a1431196..aa92d688a0d8 100644 --- a/src/Illuminate/Foundation/Configuration/Exceptions.php +++ b/src/Illuminate/Foundation/Configuration/Exceptions.php @@ -150,6 +150,19 @@ public function dontReport(array|string $class) return $this; } + /** + * Register a callback to determine if an exception should not be reported. + * + * @param (\Closure(\Throwable): bool) $dontReportWhen + * @return $this + */ + public function dontReportWhen(Closure $dontReportWhen) + { + $this->handler->dontReportWhen($dontReportWhen); + + return $this; + } + /** * Do not report duplicate exceptions. * diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 7b2e43c0c5be..6cc442e8c3c7 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -62,7 +62,7 @@ class BroadcastingInstallCommand extends Command /** * Execute the console command. * - * @return int + * @return void */ public function handle() { @@ -116,6 +116,18 @@ public function handle() 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, + ); + } } } @@ -355,9 +367,7 @@ protected function installReverb() return; } - $install = confirm('Would you like to install Laravel Reverb?', default: true); - - if (! $install) { + if (! confirm('Would you like to install Laravel Reverb?', default: true)) { return; } 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/ConfigPublishCommand.php b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php index 7053830a17c6..fc24a425f0c5 100644 --- a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php @@ -48,9 +48,7 @@ public function handle() $name = (string) (is_null($this->argument('name')) ? select( label: 'Which configuration file would you like to publish?', - options: (new Collection($config))->map(function (string $path) { - return basename($path, '.php'); - }), + options: (new Collection($config))->map(fn (string $path) => basename($path, '.php')), ) : $this->argument('name')); if (! is_null($name) && ! isset($config[$name])) { 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/Kernel.php b/src/Illuminate/Foundation/Console/Kernel.php index 55874df4d9b0..6dcd2b24935f 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -408,7 +408,7 @@ public function registerCommand($command) /** * Run an Artisan console command by name. * - * @param string $command + * @param \Symfony\Component\Console\Command\Command|string $command * @param array $parameters * @param \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer * @return int diff --git a/src/Illuminate/Foundation/Console/ModelMakeCommand.php b/src/Illuminate/Foundation/Console/ModelMakeCommand.php index 5fd029b8cad8..c63dd32e30d7 100644 --- a/src/Illuminate/Foundation/Console/ModelMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ModelMakeCommand.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function Laravel\Prompts\confirm; use function Laravel\Prompts\multiselect; #[AsCommand(name: 'make:model')] @@ -47,7 +48,15 @@ class ModelMakeCommand extends GeneratorCommand public function handle() { if (parent::handle() === false && ! $this->option('force')) { - return false; + if (! $this->alreadyExists($this->getNameInput())) { + return false; + } + + if (! confirm('Do you want to generate additional components for the model?')) { + return false; + } else { + $this->afterPromptingForMissingArguments($this->input, $this->output); + } } if ($this->option('all')) { 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/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/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 00a69266dcc7..52342f698e80 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -72,6 +72,13 @@ class Handler implements ExceptionHandlerContract */ protected $dontReport = []; + /** + * The callbacks that inspect exceptions to determine if they should be reported. + * + * @var array + */ + protected $dontReportCallbacks = []; + /** * The callbacks that should be used during reporting. * @@ -279,6 +286,23 @@ public function dontReport(array|string $exceptions) return $this->ignore($exceptions); } + /** + * Register a callback to determine if an exception should not be reported. + * + * @param (callable(\Throwable): bool) $dontReportWhen + * @return $this + */ + public function dontReportWhen(callable $dontReportWhen) + { + if (! $dontReportWhen instanceof Closure) { + $dontReportWhen = Closure::fromCallable($dontReportWhen); + } + + $this->dontReportCallbacks[] = $dontReportWhen; + + return $this; + } + /** * Indicate that the given exception type should not be reported. * @@ -413,6 +437,12 @@ protected function shouldntReport(Throwable $e) return true; } + foreach ($this->dontReportCallbacks as $dontReportCallback) { + if ($dontReportCallback($e) === true) { + return true; + } + } + return rescue(fn () => with($this->throttle($e), function ($throttle) use ($e) { if ($throttle instanceof Unlimited || $throttle === null) { return false; diff --git a/src/Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php b/src/Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php index f96d231e1db3..f1dedbbace9b 100644 --- a/src/Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php +++ b/src/Illuminate/Foundation/Exceptions/RegisterErrorViewPaths.php @@ -14,8 +14,10 @@ class RegisterErrorViewPaths */ public function __invoke() { - View::replaceNamespace('errors', (new Collection(config('view.paths')))->map(function ($path) { - return "{$path}/errors"; - })->push(__DIR__.'/views')->all()); + View::replaceNamespace('errors', (new Collection(config('view.paths'))) + ->map(fn ($path) => "{$path}/errors") + ->push(__DIR__.'/views') + ->all() + ); } } diff --git a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php index 1c20d22051b1..c3944d6c72f7 100644 --- a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -127,14 +127,14 @@ protected function hasValidBypassCookie($request, array $data) } /** - * Redirect the user back to the root of the application with a maintenance mode bypass cookie. + * Redirect the user to their intended destination with a maintenance mode bypass cookie. * * @param string $secret * @return \Illuminate\Http\RedirectResponse */ protected function bypassResponse(string $secret) { - return redirect('/')->withCookie( + return redirect()->intended('/')->withCookie( MaintenanceModeBypassCookie::create($secret) ); } diff --git a/src/Illuminate/Foundation/Inspiring.php b/src/Illuminate/Foundation/Inspiring.php index 59891493d9b5..3d4c6d83ef25 100644 --- a/src/Illuminate/Foundation/Inspiring.php +++ b/src/Illuminate/Foundation/Inspiring.php @@ -106,6 +106,7 @@ public static function quotes() 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk', 'Always remember that you are absolutely unique. Just like everyone else. - Margaret Mead', 'You must be the change you wish to see in the world. - Mahatma Gandhi', + 'It always seems impossible until it is done. - Nelson Mandela', 'We must ship. - Taylor Otwell', ]); } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php index 9e8c0f5870b6..fc1b13e7c67a 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php @@ -18,6 +18,21 @@ public function actingAs(UserContract $user, $guard = null) return $this->be($user, $guard); } + /** + * Clear the currently logged in user for the application. + * + * @param string|null $guard + * @return $this + */ + public function actingAsGuest($guard = null) + { + $this->app['auth']->guard($guard)->forgetUser(); + + $this->app['auth']->shouldUse($guard); + + return $this; + } + /** * Set the currently logged in user for the application. * diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 89a0c3ac9797..e5ac69c289dd 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; @@ -19,13 +18,21 @@ trait InteractsWithDatabase /** * Assert that a given where condition exists in the database. * - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @return $this */ protected function assertDatabaseHas($table, array $data = [], $connection = null) { + if (is_iterable($table)) { + foreach ($table as $item) { + $this->assertDatabaseHas($item, $data, $connection); + } + + return $this; + } + if ($table instanceof Model) { $data = [ $table->getKeyName() => $table->getKey(), @@ -43,13 +50,21 @@ protected function assertDatabaseHas($table, array $data = [], $connection = nul /** * Assert that a given where condition does not exist in the database. * - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @return $this */ protected function assertDatabaseMissing($table, array $data = [], $connection = null) { + if (is_iterable($table)) { + foreach ($table as $item) { + $this->assertDatabaseMissing($item, $data, $connection); + } + + return $this; + } + if ($table instanceof Model) { $data = [ $table->getKeyName() => $table->getKey(), @@ -102,7 +117,7 @@ protected function assertDatabaseEmpty($table, $connection = null) /** * Assert the given record has been "soft deleted". * - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @param string|null $deletedAtColumn @@ -110,6 +125,14 @@ protected function assertDatabaseEmpty($table, $connection = null) */ protected function assertSoftDeleted($table, array $data = [], $connection = null, $deletedAtColumn = 'deleted_at') { + if (is_iterable($table)) { + foreach ($table as $item) { + $this->assertSoftDeleted($item, $data, $connection); + } + + return $this; + } + if ($this->isSoftDeletableModel($table)) { return $this->assertSoftDeleted( $table->getTable(), @@ -134,7 +157,7 @@ protected function assertSoftDeleted($table, array $data = [], $connection = nul /** * Assert the given record has not been "soft deleted". * - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @param string|null $deletedAtColumn @@ -142,6 +165,14 @@ protected function assertSoftDeleted($table, array $data = [], $connection = nul */ protected function assertNotSoftDeleted($table, array $data = [], $connection = null, $deletedAtColumn = 'deleted_at') { + if (is_iterable($table)) { + foreach ($table as $item) { + $this->assertNotSoftDeleted($item, $data, $connection); + } + + return $this; + } + if ($this->isSoftDeletableModel($table)) { return $this->assertNotSoftDeleted( $table->getTable(), @@ -166,7 +197,7 @@ protected function assertNotSoftDeleted($table, array $data = [], $connection = /** * Assert the given model exists in the database. * - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model + * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model * @return $this */ protected function assertModelExists($model) @@ -177,7 +208,7 @@ protected function assertModelExists($model) /** * Assert the given model does not exist in the database. * - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model + * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model * @return $this */ protected function assertModelMissing($model) @@ -223,8 +254,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/MakesHttpRequests.php b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php index 3c37c95e4e00..1ace8556feef 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -702,9 +702,10 @@ protected function prepareCookiesForRequest() return array_merge($this->defaultCookies, $this->unencryptedCookies); } - return (new Collection($this->defaultCookies))->map(function ($value, $key) { - return encrypt(CookieValuePrefix::create($key, app('encrypter')->getKey()).$value, false); - })->merge($this->unencryptedCookies)->all(); + return (new Collection($this->defaultCookies)) + ->map(fn ($value, $key) => encrypt(CookieValuePrefix::create($key, app('encrypter')->getKey()).$value, false)) + ->merge($this->unencryptedCookies) + ->all(); } /** diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 7b7de434c27a..81278816abb8 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -693,6 +693,7 @@ protected function resolvePreloadTagAttributes($src, $url, $chunk, $manifest) 'crossorigin' => $this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false, ] : [ 'rel' => 'modulepreload', + 'as' => 'script', 'href' => $url, 'nonce' => $this->nonce ?? false, 'crossorigin' => $this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false, diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index 8f0523dce09f..62518b5af01d 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 +226,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 +441,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 +548,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 +578,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. @@ -593,12 +624,12 @@ function mix($path, $manifestDirectory = '') /** * Create a new Carbon instance for the current time. * - * @param \DateTimeZone|string|null $tz + * @param \DateTimeZone|\UnitEnum|string|null $tz * @return \Illuminate\Support\Carbon */ function now($tz = null) { - return Date::now($tz); + return Date::now(enum_value($tz)); } } @@ -799,7 +830,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 +857,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 = []) @@ -921,6 +950,22 @@ function storage_path($path = '') } } +if (! function_exists('to_action')) { + /** + * Create a new redirect response to a controller action. + * + * @param string|array $action + * @param mixed $parameters + * @param int $status + * @param array $headers + * @return \Illuminate\Http\RedirectResponse + */ + function to_action($action, $parameters = [], $status = 302, $headers = []) + { + return redirect()->action($action, $parameters, $status, $headers); + } +} + if (! function_exists('to_route')) { /** * Create a new redirect response to a named route. @@ -941,12 +986,12 @@ function to_route($route, $parameters = [], $status = 302, $headers = []) /** * Create a new Carbon instance for the current date. * - * @param \DateTimeZone|string|null $tz + * @param \DateTimeZone|\UnitEnum|string|null $tz * @return \Illuminate\Support\Carbon */ function today($tz = null) { - return Date::today($tz); + return Date::today(enum_value($tz)); } } @@ -975,7 +1020,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 +1085,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/Factory.php b/src/Illuminate/Http/Client/Factory.php index 49391a4fa1bb..68df34ed973a 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -469,12 +469,13 @@ public function recorded($callback = null) return new Collection; } - $callback = $callback ?: function () { - return true; - }; + $collect = new Collection($this->recorded); + + if ($callback) { + return $collect->filter(fn ($pair) => $callback($pair[0], $pair[1])); + } - return (new Collection($this->recorded)) - ->filter(fn ($pair) => $callback($pair[0], $pair[1])); + return $collect; } /** diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 7abcb3a459b8..66fc30286b29 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. * @@ -464,6 +471,20 @@ public function withDigestAuth($username, $password) }); } + /** + * Specify the NTLM authentication username and password for the request. + * + * @param string $username + * @param string $password + * @return $this + */ + public function withNtlmAuth($username, $password) + { + return tap($this, function () use ($username, $password) { + $this->options['auth'] = [$username, $password, 'ntlm']; + }); + } + /** * Specify an authorization token for the request. * @@ -935,15 +956,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); @@ -1009,7 +1035,21 @@ protected function parseHttpOptions(array $options) protected function parseMultipartBodyFormat(array $data) { return (new Collection($data)) - ->map(fn ($value, $key) => is_array($value) ? $value : ['name' => $key, 'contents' => $value]) + ->flatMap(function ($value, $key) { + if (is_array($value)) { + // If the array has 'name' and 'contents' keys, it's already formatted for multipart... + if (isset($value['name']) && isset($value['contents'])) { + return [$value]; + } + + // Otherwise, treat it as multiple values for the same field name... + return (new Collection($value))->map(function ($item) use ($key) { + return ['name' => $key.'[]', 'contents' => $item]; + }); + } + + return [['name' => $key, 'contents' => $value]]; + }) ->values() ->all(); } @@ -1424,7 +1464,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 +1565,91 @@ 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); + + $request = new Request($e->getRequest()); + + $this->factory?->recordRequestResponsePair( + $request, 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); + + $request = new Request($e->getRequest()); + + $this->factory?->recordRequestResponsePair( + $request, null + ); + + $this->dispatchConnectionFailedEvent($request, $exception); + + throw $exception; + } + + /** + * Handle the given request exception. + * + * @param \GuzzleHttp\Exception\RequestException $e + * @return void + */ + protected function marshalRequestExceptionWithResponse(RequestException $e) + { + $response = $this->populateResponse($this->newResponse($e->getResponse())); + + $this->factory?->recordRequestResponsePair( + new Request($e->getRequest()), + $response + ); + + 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/Http/Middleware/TrustProxies.php b/src/Illuminate/Http/Middleware/TrustProxies.php index 0e6936d56a54..93ac857d4d46 100644 --- a/src/Illuminate/Http/Middleware/TrustProxies.php +++ b/src/Illuminate/Http/Middleware/TrustProxies.php @@ -68,7 +68,10 @@ protected function setTrustedProxyIpAddresses(Request $request) { $trustedIps = $this->proxies() ?: config('trustedproxy.proxies'); - if (is_null($trustedIps) && laravel_cloud()) { + if (is_null($trustedIps) && + (laravel_cloud() || + str_ends_with($request->host(), '.on-forge.com') || + str_ends_with($request->host(), '.on-vapor.com'))) { $trustedIps = '*'; } diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index d64082868105..d355cab241d2 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -22,6 +22,9 @@ * @method array validate(array $rules, ...$params) * @method array validateWithBag(string $errorBag, array $rules, ...$params) * @method bool hasValidSignature(bool $absolute = true) + * @method bool hasValidRelativeSignature() + * @method bool hasValidSignatureWhileIgnoring($ignoreQuery = [], $absolute = true) + * @method bool hasValidRelativeSignatureWhileIgnoring($ignoreQuery = []) */ class Request extends SymfonyRequest implements Arrayable, ArrayAccess { diff --git a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php index 16e026986484..9eeff4a5f1dd 100644 --- a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php +++ b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php @@ -100,13 +100,13 @@ protected function removeMissingValues($data) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function when($condition, $value, $default = null) + protected function when($condition, $value, $default = new MissingValue) { if ($condition) { return value($value); } - return func_num_args() === 3 ? value($default) : new MissingValue; + return func_num_args() === 3 ? value($default) : $default; } /** @@ -117,7 +117,7 @@ protected function when($condition, $value, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - public function unless($condition, $value, $default = null) + public function unless($condition, $value, $default = new MissingValue) { $arguments = func_num_args() === 2 ? [$value] : [$value, $default]; @@ -143,13 +143,13 @@ protected function merge($value) * @param mixed $default * @return \Illuminate\Http\Resources\MergeValue|mixed */ - protected function mergeWhen($condition, $value, $default = null) + protected function mergeWhen($condition, $value, $default = new MissingValue) { if ($condition) { return new MergeValue(value($value)); } - return func_num_args() === 3 ? new MergeValue(value($default)) : new MissingValue(); + return func_num_args() === 3 ? new MergeValue(value($default)) : $default; } /** @@ -160,7 +160,7 @@ protected function mergeWhen($condition, $value, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MergeValue|mixed */ - protected function mergeUnless($condition, $value, $default = null) + protected function mergeUnless($condition, $value, $default = new MissingValue) { $arguments = func_num_args() === 2 ? [$value] : [$value, $default]; @@ -188,12 +188,8 @@ protected function attributes($attributes) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - public function whenHas($attribute, $value = null, $default = null) + public function whenHas($attribute, $value = null, $default = new MissingValue) { - if (func_num_args() < 3) { - $default = new MissingValue; - } - if (! array_key_exists($attribute, $this->resource->getAttributes())) { return value($default); } @@ -210,7 +206,7 @@ public function whenHas($attribute, $value = null, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function whenNull($value, $default = null) + protected function whenNull($value, $default = new MissingValue) { $arguments = func_num_args() == 1 ? [$value] : [$value, $default]; @@ -224,7 +220,7 @@ protected function whenNull($value, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function whenNotNull($value, $default = null) + protected function whenNotNull($value, $default = new MissingValue) { $arguments = func_num_args() == 1 ? [$value] : [$value, $default]; @@ -239,13 +235,13 @@ protected function whenNotNull($value, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function whenAppended($attribute, $value = null, $default = null) + protected function whenAppended($attribute, $value = null, $default = new MissingValue) { if ($this->resource->hasAppended($attribute)) { return func_num_args() >= 2 ? value($value) : $this->resource->$attribute; } - return func_num_args() === 3 ? value($default) : new MissingValue; + return func_num_args() === 3 ? value($default) : $default; } /** @@ -256,12 +252,8 @@ protected function whenAppended($attribute, $value = null, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function whenLoaded($relationship, $value = null, $default = null) + protected function whenLoaded($relationship, $value = null, $default = new MissingValue) { - if (func_num_args() < 3) { - $default = new MissingValue; - } - if (! $this->resource->relationLoaded($relationship)) { return value($default); } @@ -291,12 +283,8 @@ protected function whenLoaded($relationship, $value = null, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - public function whenCounted($relationship, $value = null, $default = null) + public function whenCounted($relationship, $value = null, $default = new MissingValue) { - if (func_num_args() < 3) { - $default = new MissingValue; - } - $attribute = (new Stringable($relationship))->snake()->finish('_count')->value(); if (! array_key_exists($attribute, $this->resource->getAttributes())) { @@ -328,12 +316,8 @@ public function whenCounted($relationship, $value = null, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - public function whenAggregated($relationship, $column, $aggregate, $value = null, $default = null) + public function whenAggregated($relationship, $column, $aggregate, $value = null, $default = new MissingValue) { - if (func_num_args() < 5) { - $default = new MissingValue; - } - $attribute = (new Stringable($relationship))->snake()->append('_')->append($aggregate)->append('_')->finish($column)->value(); if (! array_key_exists($attribute, $this->resource->getAttributes())) { @@ -363,12 +347,8 @@ public function whenAggregated($relationship, $column, $aggregate, $value = null * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - public function whenExistsLoaded($relationship, $value = null, $default = null) + public function whenExistsLoaded($relationship, $value = null, $default = new MissingValue) { - if (func_num_args() < 3) { - $default = new MissingValue; - } - $attribute = (new Stringable($relationship))->snake()->finish('_exists')->value(); if (! array_key_exists($attribute, $this->resource->getAttributes())) { @@ -394,7 +374,7 @@ public function whenExistsLoaded($relationship, $value = null, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function whenPivotLoaded($table, $value, $default = null) + protected function whenPivotLoaded($table, $value, $default = new MissingValue) { return $this->whenPivotLoadedAs('pivot', ...func_get_args()); } @@ -408,12 +388,8 @@ protected function whenPivotLoaded($table, $value, $default = null) * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ - protected function whenPivotLoadedAs($accessor, $table, $value, $default = null) + protected function whenPivotLoadedAs($accessor, $table, $value, $default = new MissingValue) { - if (func_num_args() === 3) { - $default = new MissingValue; - } - return $this->when( $this->hasPivotLoadedAs($accessor, $table), $value, @@ -443,7 +419,7 @@ protected function hasPivotLoadedAs($accessor, $table) { return isset($this->resource->$accessor) && ($this->resource->$accessor instanceof $table || - $this->resource->$accessor->getTable() === $table); + $this->resource->$accessor->getTable() === $table); } /** @@ -454,10 +430,10 @@ protected function hasPivotLoadedAs($accessor, $table) * @param mixed $default * @return mixed */ - protected function transform($value, callable $callback, $default = null) + protected function transform($value, callable $callback, $default = new MissingValue) { return transform( - $value, $callback, func_num_args() === 3 ? $default : new MissingValue + $value, $callback, $default ); } } diff --git a/src/Illuminate/Http/Resources/DelegatesToResource.php b/src/Illuminate/Http/Resources/DelegatesToResource.php index e932646e19af..fdb05db3134d 100644 --- a/src/Illuminate/Http/Resources/DelegatesToResource.php +++ b/src/Illuminate/Http/Resources/DelegatesToResource.php @@ -58,7 +58,7 @@ public function resolveRouteBinding($value, $field = null) */ public function resolveChildRouteBinding($childType, $value, $field = null) { - throw new Exception('Resources may not be implicitly resolved from route bindings.'); + throw new Exception('Resources may not be implicitly resolved from child route bindings.'); } /** 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/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 270b716f9d8d..2987694f1941 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -271,17 +271,21 @@ protected function createStackDriver(array $config) $config['channels'] = explode(',', $config['channels']); } - $handlers = (new Collection($config['channels']))->flatMap(function ($channel) { - return $channel instanceof LoggerInterface - ? $channel->getHandlers() - : $this->channel($channel)->getHandlers(); - })->all(); - - $processors = (new Collection($config['channels']))->flatMap(function ($channel) { - return $channel instanceof LoggerInterface - ? $channel->getProcessors() - : $this->channel($channel)->getProcessors(); - })->all(); + $handlers = (new Collection($config['channels'])) + ->flatMap(function ($channel) { + return $channel instanceof LoggerInterface + ? $channel->getHandlers() + : $this->channel($channel)->getHandlers(); + }) + ->all(); + + $processors = (new Collection($config['channels'])) + ->flatMap(function ($channel) { + return $channel instanceof LoggerInterface + ? $channel->getProcessors() + : $this->channel($channel)->getProcessors(); + }) + ->all(); if ($config['ignore_exceptions'] ?? false) { $handlers = [new WhatFailureGroupHandler($handlers)]; 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 38318992c7f0..2106b1750893 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -1184,13 +1184,17 @@ public function hasTag($value) /** * Add a metadata header to the message when supported by the underlying transport. * - * @param string $key - * @param string $value + * @param array|string $key + * @param string|null $value * @return $this */ - public function metadata($key, $value) + public function metadata($key, $value = null) { - $this->metadata[$key] = $value; + if (is_array($key)) { + $this->metadata = array_merge($this->metadata, $key); + } else { + $this->metadata[$key] = $value; + } return $this; } @@ -1219,11 +1223,12 @@ public function assertFrom($address, $name = null) { $this->renderForAssertions(); - $recipient = $this->formatAssertionRecipient($address, $name); + $expected = $this->formatAssertionRecipient($address, $name); + $actual = $this->formatActualRecipients($this->from); PHPUnit::assertTrue( $this->hasFrom($address, $name), - "Email was not from expected address [{$recipient}]." + "Email was not from expected address.\nExpected: [{$expected}]\nActual: [{$actual}]" ); return $this; @@ -1240,11 +1245,12 @@ public function assertTo($address, $name = null) { $this->renderForAssertions(); - $recipient = $this->formatAssertionRecipient($address, $name); + $expected = $this->formatAssertionRecipient($address, $name); + $actual = $this->formatActualRecipients($this->to); PHPUnit::assertTrue( $this->hasTo($address, $name), - "Did not see expected recipient [{$recipient}] in email 'to' recipients." + "Did not see expected recipient in email 'to' recipients.\nExpected: [{$expected}]\nActual: [{$actual}]" ); return $this; @@ -1273,11 +1279,12 @@ public function assertHasCc($address, $name = null) { $this->renderForAssertions(); - $recipient = $this->formatAssertionRecipient($address, $name); + $expected = $this->formatAssertionRecipient($address, $name); + $actual = $this->formatActualRecipients($this->cc); PHPUnit::assertTrue( $this->hasCc($address, $name), - "Did not see expected recipient [{$recipient}] in email 'cc' recipients." + "Did not see expected recipient in email 'cc' recipients.\nExpected: [{$expected}]\nActual: [{$actual}]" ); return $this; @@ -1294,11 +1301,12 @@ public function assertHasBcc($address, $name = null) { $this->renderForAssertions(); - $recipient = $this->formatAssertionRecipient($address, $name); + $expected = $this->formatAssertionRecipient($address, $name); + $actual = $this->formatActualRecipients($this->bcc); PHPUnit::assertTrue( $this->hasBcc($address, $name), - "Did not see expected recipient [{$recipient}] in email 'bcc' recipients." + "Did not see expected recipient in email 'bcc' recipients.\nExpected: [{$expected}]\nActual: [{$actual}]" ); return $this; @@ -1315,11 +1323,12 @@ public function assertHasReplyTo($address, $name = null) { $this->renderForAssertions(); - $replyTo = $this->formatAssertionRecipient($address, $name); + $expected = $this->formatAssertionRecipient($address, $name); + $actual = $this->formatActualRecipients($this->replyTo); PHPUnit::assertTrue( $this->hasReplyTo($address, $name), - "Did not see expected address [{$replyTo}] as email 'reply to' recipient." + "Did not see expected address as email 'reply to' recipient.\nExpected: [{$expected}]\nActual: [{$actual}]" ); return $this; @@ -1345,6 +1354,28 @@ private function formatAssertionRecipient($address, $name = null) return $address; } + /** + * Format actual recipients for display in assertion messages. + * + * @param array $recipients + * @return string + */ + private function formatActualRecipients($recipients) + { + if (empty($recipients)) { + return 'none'; + } + + return (new Collection($recipients))->map(function ($recipient) { + $formatted = $recipient['address']; + if (! empty($recipient['name'])) { + $formatted .= ' ('.$recipient['name'].')'; + } + + return $formatted; + })->implode(', '); + } + /** * Assert that the mailable has the given subject. * @@ -1355,9 +1386,11 @@ public function assertHasSubject($subject) { $this->renderForAssertions(); + $actualSubject = $this->subject ?: (method_exists($this, 'envelope') ? $this->envelope()->subject : null) ?: Str::title(Str::snake(class_basename($this), ' ')); + PHPUnit::assertTrue( $this->hasSubject($subject), - "Did not see expected text [{$subject}] in email subject." + "Email subject does not match expected value.\nExpected: [{$subject}]\nActual: [{$actualSubject}]" ); return $this; @@ -1570,9 +1603,12 @@ public function assertHasTag($tag) { $this->renderForAssertions(); + $actualTags = method_exists($this, 'envelope') ? array_merge($this->tags, $this->envelope()->tags) : $this->tags; + $actualTagsString = empty($actualTags) ? 'none' : implode(', ', $actualTags); + PHPUnit::assertTrue( $this->hasTag($tag), - "Did not see expected tag [{$tag}] in email tags." + "Did not see expected tag in email tags.\nExpected: [{$tag}]\nActual: [{$actualTagsString}]" ); return $this; @@ -1589,9 +1625,13 @@ public function assertHasMetadata($key, $value) { $this->renderForAssertions(); + $actualMetadata = method_exists($this, 'envelope') ? array_merge($this->metadata, $this->envelope()->metadata) : $this->metadata; + $actualValue = $actualMetadata[$key] ?? null; + $actualString = $actualValue !== null ? "[{$key}] => [{$actualValue}]" : "key [{$key}] not found"; + PHPUnit::assertTrue( $this->hasMetadata($key, $value), - "Did not see expected key [{$key}] and value [{$value}] in email metadata." + "Email metadata does not match expected value.\nExpected: [{$key}] => [{$value}]\nActual: {$actualString}" ); return $this; @@ -1775,6 +1815,17 @@ private function ensureAttachmentsAreHydrated() }); } + /** + * Determine if the mailable will be sent by the given mailer. + * + * @param string $mailer + * @return bool + */ + public function usesMailer($mailer) + { + return $this->mailer === $mailer; + } + /** * Set the name of the mailer that should send the message. * 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/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..fea9fdc22574 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. @@ -402,8 +403,12 @@ public function items() /** * Transform each item in the slice of items using a callback. * - * @param callable $callback + * @template TThroughValue + * + * @param callable(TValue, TKey): TThroughValue $callback * @return $this + * + * @phpstan-this-out static */ public function through(callable $callback) { diff --git a/src/Illuminate/Pagination/AbstractPaginator.php b/src/Illuminate/Pagination/AbstractPaginator.php index b27830f84ecc..bce700f71975 100644 --- a/src/Illuminate/Pagination/AbstractPaginator.php +++ b/src/Illuminate/Pagination/AbstractPaginator.php @@ -353,8 +353,12 @@ public function lastItem() /** * Transform each item in the slice of items using a callback. * - * @param callable $callback + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback * @return $this + * + * @phpstan-this-out static */ public function through(callable $callback) { diff --git a/src/Illuminate/Pagination/Cursor.php b/src/Illuminate/Pagination/Cursor.php index 73e5cd4e3a56..433e33e0ae1c 100644 --- a/src/Illuminate/Pagination/Cursor.php +++ b/src/Illuminate/Pagination/Cursor.php @@ -60,9 +60,9 @@ public function parameter(string $parameterName) */ public function parameters(array $parameterNames) { - return (new Collection($parameterNames))->map(function ($parameterName) { - return $this->parameter($parameterName); - })->toArray(); + return (new Collection($parameterNames)) + ->map(fn ($parameterName) => $this->parameter($parameterName)) + ->toArray(); } /** 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()) -