diff --git a/.gitattributes b/.gitattributes index ba7452152c0d..8382fc5c826f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,7 +17,7 @@ .gitattributes export-ignore .gitignore export-ignore .styleci.yml export-ignore -CHANGELOG-* export-ignore +CHANGELOG.md export-ignore CODE_OF_CONDUCT.md export-ignore CONTRIBUTING.md export-ignore docker-compose.yml export-ignore diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 961af6983a22..6172854ee02f 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -147,6 +147,55 @@ jobs: env: DB_CONNECTION: mariadb + pgsql_18: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + services: + postgresql: + image: postgres:18 + env: + POSTGRES_DB: laravel + POSTGRES_USER: forge + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 + + strategy: + fail-fast: true + + name: PostgreSQL 18 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, :php-psr + tools: composer:v2 + coverage: none + + - name: Set Framework version + run: composer config version "12.x-dev" + + - name: Install dependencies + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Database + env: + DB_CONNECTION: pgsql + DB_USERNAME: forge + DB_PASSWORD: password + pgsql_14: runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -293,53 +342,53 @@ jobs: DB_USERNAME: SA DB_PASSWORD: Forge123 - # mssql_2017: - # runs-on: ubuntu-20.04 - # timeout-minutes: 5 - - # services: - # sqlsrv: - # image: mcr.microsoft.com/mssql/server:2017-latest - # env: - # ACCEPT_EULA: Y - # SA_PASSWORD: Forge123 - # ports: - # - 1433:1433 - - # strategy: - # fail-fast: true - - # name: SQL Server 2017 - - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - - # - name: Setup PHP - # uses: shivammathur/setup-php@v2 - # with: - # php-version: 8.3 - # extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr - # tools: composer:v2 - # coverage: none - - # - name: Set Framework version - # run: composer config version "12.x-dev" - - # - name: Install dependencies - # uses: nick-fields/retry@v3 - # with: - # timeout_minutes: 5 - # max_attempts: 5 - # command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - - # - name: Execute tests - # run: vendor/bin/phpunit tests/Integration/Database - # env: - # DB_CONNECTION: sqlsrv - # DB_DATABASE: master - # DB_USERNAME: SA - # DB_PASSWORD: Forge123 + mssql_2017: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + services: + sqlsrv: + image: mcr.microsoft.com/mssql/server:2017-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: Forge123 + ports: + - 1433:1433 + + strategy: + fail-fast: true + + name: SQL Server 2017 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr + tools: composer:v2 + coverage: none + + - name: Set Framework version + run: composer config version "12.x-dev" + + - name: Install dependencies + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Database + env: + DB_CONNECTION: sqlsrv + DB_DATABASE: master + DB_USERNAME: SA + DB_PASSWORD: Forge123 sqlite: runs-on: ubuntu-24.04 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a156dd33351..952e51424b53 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: ports: - 11211:11211 mysql: - image: mysql:5.7 + image: mysql:8 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: forge @@ -40,13 +40,23 @@ 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.4.0'] stability: [prefer-lowest, prefer-stable] exclude: - php: 8.2 phpunit: '12.0.0' - php: 8.2 + phpunit: '12.4.0' + include: + - php: 8.3 phpunit: '12.1.0' + stability: prefer-stable + - php: 8.3 + phpunit: '12.2.0' + stability: prefer-stable + - php: 8.3 + phpunit: '12.3.0' + stability: prefer-stable name: PHP ${{ matrix.php }} - PHPUnit ${{ matrix.phpunit }} - ${{ matrix.stability }} @@ -101,13 +111,23 @@ 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.4.0'] stability: [prefer-lowest, prefer-stable] exclude: - php: 8.2 phpunit: '12.0.0' - php: 8.2 + phpunit: '12.4.0' + include: + - php: 8.3 phpunit: '12.1.0' + stability: prefer-stable + - php: 8.3 + phpunit: '12.2.0' + stability: prefer-stable + - php: 8.3 + phpunit: '12.3.0' + stability: prefer-stable name: PHP ${{ matrix.php }} - PHPUnit ${{ matrix.phpunit }} - ${{ matrix.stability }} - Windows diff --git a/.github/workflows/update-assets.yml b/.github/workflows/update-assets.yml index 8ecd63efce0a..1d86e9e71433 100644 --- a/.github/workflows/update-assets.yml +++ b/.github/workflows/update-assets.yml @@ -3,10 +3,18 @@ name: 'update assets' on: push: branches: - - '12.x' + - master + - '*.x' paths: - - '/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json' - workflow_dispatch: + - 'src/Illuminate/Foundation/resources/exceptions/renderer/**' + - '!src/Illuminate/Foundation/resources/exceptions/renderer/dist/**' + pull_request: + paths: + - 'src/Illuminate/Foundation/resources/exceptions/renderer/**' + - '!src/Illuminate/Foundation/resources/exceptions/renderer/dist/**' + +permissions: + contents: write jobs: update: @@ -18,7 +26,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 - name: Update Exception Renderer Assets run: | diff --git a/.gitignore b/.gitignore index 39397245b7ec..496161c0903d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ +.DS_Store +.phpunit.result.cache +/.fleet +/.idea /.phpunit.cache +/phpunit.xml +/.vscode /vendor composer.phar composer.lock -.DS_Store Thumbs.db -/phpunit.xml -/.idea -/.fleet -/.vscode -.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index b292512610ef..b861f9ebde22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,704 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.10.2...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.34.0...12.x) + +## [v12.34.0](https://github.com/laravel/framework/compare/v12.33.0...v12.34.0) - 2025-10-14 + +* [12.x] PostgreSQL virtual columns by [@tpetry](https://github.com/tpetry) in https://github.com/laravel/framework/pull/57290 +* [12.x] Make Vite asset path generation extendable via inheritance by [@daun](https://github.com/daun) in https://github.com/laravel/framework/pull/57292 +* [12.x] Improve `Str` docblocks related to factories by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57297 +* Add missing waitUntil method to FakeInvokedProcess by [@yondifon](https://github.com/yondifon) in https://github.com/laravel/framework/pull/57030 +* Add support for Zed Editor in ResolvesDumpSource by [@miguilimzero](https://github.com/miguilimzero) in https://github.com/laravel/framework/pull/57298 +* [12.x] Remove leftover workaround by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57306 +* Fix return type order in view function signature by [@MadBox-99](https://github.com/MadBox-99) in https://github.com/laravel/framework/pull/57304 +* Adds support for `Trae IDE` in the local exception page by [@sajjadhossainshohag](https://github.com/sajjadhossainshohag) in https://github.com/laravel/framework/pull/57300 +* [12.x] Add enum support to `Schedule::useCache()` by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/57311 +* [12.x] Fix remaining PHP 8.5 null index array deprecations by [@IonBazan](https://github.com/IonBazan) in https://github.com/laravel/framework/pull/57308 +* Regenerate session during Auth::login() by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/57204 +* [12.x] Formatting by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57321 +* Update text color in minimal error view to ensure better accessibility by [@FoksVHox](https://github.com/FoksVHox) in https://github.com/laravel/framework/pull/57318 +* [12.x] Fix text truncation on syntax-highlighted queries by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57315 +* [12.x] Fix email rule helper message by [@erik-perri](https://github.com/erik-perri) in https://github.com/laravel/framework/pull/57323 +* [12.x] Do not assume `Str::uuid()` returns `Stringable` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57340 +* [12.x] Add missing [@throws](https://github.com/throws) annotation to Arr by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57336 +* [12.x] Use FQCN in docblocks by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57335 +* [12.x] feat: Support custom response without modifying the exception handler by [@chuoke](https://github.com/chuoke) in https://github.com/laravel/framework/pull/57342 +* [12.X] add support for windsurf IDE in ResolvesDumpSource by [@Sajid-al-islam](https://github.com/Sajid-al-islam) in https://github.com/laravel/framework/pull/57359 +* [12.x] Expand single-line array into multiline by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57350 +* [12.x] Added Kiro editor support in `ResolvesDumpSource` by [@OmarFaruk-0x01](https://github.com/OmarFaruk-0x01) in https://github.com/laravel/framework/pull/57363 +* [12.x] fix schedule list cli format in multibye locale by [@jamessa](https://github.com/jamessa) in https://github.com/laravel/framework/pull/57367 +* Prototype failover queue by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57341 +* Add support for Fleet editor in ResolvesDumpSource by [@Rakib01](https://github.com/Rakib01) in https://github.com/laravel/framework/pull/57377 +* Allow closures when calling throw_if by [@chrispage1](https://github.com/chrispage1) in https://github.com/laravel/framework/pull/57349 +* [12.x] Add defer method to HTTP batch by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/57387 +* [12.x] Supports PHPUnit 12.4 by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57388 +* [12.x] Http::batch - fix issue that non valid URL not triggering catch hook by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/57386 + +## [v12.33.0](https://github.com/laravel/framework/compare/v12.32.5...v12.33.0) - 2025-10-07 + +* Fix compiling queries that use orderByRaw with expressions by [@LukeTowers](https://github.com/LukeTowers) in https://github.com/laravel/framework/pull/57228 +* [12.x] Narrow type after `Str::is*(...)` check by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/57230 +* [12.x] Fix invalid docblock by [@tm1000](https://github.com/tm1000) in https://github.com/laravel/framework/pull/57240 +* [12.x] Refactor switch to match by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/57236 +* [12.x] Refactor switch to match by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57237 +* [12.x] Fix rounded issue in exception frame component by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57239 +* [12.x] Ensure calling job within a group works as expected by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57224 +* fix: remove duplicated word in `Str::apa` method by [@balboacodes](https://github.com/balboacodes) in https://github.com/laravel/framework/pull/57254 +* refactor: add |null in docblock by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57253 +* [12.x] Improve `php artisan config:cache` and `php artisan optimize` error messages for non-serializable values by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/57249 +* [12.x] Ensure cookie lifetime matches session lifetime in StartSession middleware by [@michaelcontento](https://github.com/michaelcontento) in https://github.com/laravel/framework/pull/57266 +* Run tests on PostgreSQL version 18 by [@JurianArie](https://github.com/JurianArie) in https://github.com/laravel/framework/pull/57232 +* [12x.] reduce repeated inserts in tests by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57273 +* [12.x] Fix using pushIf blade directive with complex conditions (#57264) by [@hosni](https://github.com/hosni) in https://github.com/laravel/framework/pull/57274 +* [12.x] Add Stringable::doesntContain() to match API symmetry by [@michaelcontento](https://github.com/michaelcontento) in https://github.com/laravel/framework/pull/57279 +* [12.x] Improve BroadcastManager error messages when trying to get a Broadcaster by [@mathiasgrimm](https://github.com/mathiasgrimm) in https://github.com/laravel/framework/pull/57275 +* [12.x] HTTP Client: add mergeUrlParameters() to combine URL parameters without overwriting by [@leek](https://github.com/leek) in https://github.com/laravel/framework/pull/57282 + +## [v12.32.5](https://github.com/laravel/framework/compare/v12.32.4...v12.32.5) - 2025-09-30 + +## [v12.32.4](https://github.com/laravel/framework/compare/v12.32.3...v12.32.4) - 2025-09-30 + +* [12.x] Use `Container::getInstance()` in `ComposerScripts::prePackageUninstall()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57226 + +## [v12.32.3](https://github.com/laravel/framework/compare/v12.32.2...v12.32.3) - 2025-09-30 + +* [12.x] Define LARAVEL_START if not already defined by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57222 +* [12.x] Clean up redundant type hints in docblocks by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57219 + +## [v12.32.2](https://github.com/laravel/framework/compare/v12.32.1...v12.32.2) - 2025-09-30 + +## [v12.32.1](https://github.com/laravel/framework/compare/v12.32.0...v12.32.1) - 2025-09-30 + +* [13.x] Fix scopedBy attribute not following inheritance chain by [@Muffinman](https://github.com/Muffinman) in https://github.com/laravel/framework/pull/57213 +* [12.x] Fix AWS S3 adapter's constructor not allowing decorated adapter instances by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57217 + +## [v12.32.0](https://github.com/laravel/framework/compare/v12.31.1...v12.32.0) - 2025-09-30 + +* [12.x] fix static analysis error by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57162 +* Fix: Handle non-string returns from Htmlable::toHtml() in e() helper by [@Carnicero90](https://github.com/Carnicero90) in https://github.com/laravel/framework/pull/57157 +* [12.x] Fix pending attributes in schedule group by [@jamessa](https://github.com/jamessa) in https://github.com/laravel/framework/pull/57156 +* Remove Request overview from Exceptions by [@barryvdh](https://github.com/barryvdh) in https://github.com/laravel/framework/pull/57158 +* [12.x] Pass "throw" option from scoped to parent disk by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57163 +* [12.x] Make docblock return type in line with actual return type by [@parijke](https://github.com/parijke) in https://github.com/laravel/framework/pull/57164 +* [12.x] Adjust `Arr` typehints by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57165 +* [12.x] Track filesystem adapter decoration by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57167 +* [12.x] Batch Job Failure Callbacks Support by [@yitzwillroth](https://github.com/yitzwillroth) in https://github.com/laravel/framework/pull/55916 +* [12.x] Fix operator precedence by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57169 +* [12.x] Clean up after filesystem manager tests by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57168 +* Fix: Improve validateInteger ergonomics and fix BC break by [@ntm-dev](https://github.com/ntm-dev) in https://github.com/laravel/framework/pull/57175 +* [12.x] Fix nested `can` and inherit models on route groups by [@bonroyage](https://github.com/bonroyage) in https://github.com/laravel/framework/pull/57172 +* [12.x] Syntax highlight on the frontend by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57184 +* [12.x] Add missing Closure type to Collection::pluck() docblock by [@Bariss61](https://github.com/Bariss61) in https://github.com/laravel/framework/pull/57178 +* Add database afterRollback callback support and tests by [@maltekuhr](https://github.com/maltekuhr) in https://github.com/laravel/framework/pull/57180 +* fix: add return type by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/57192 +* [12.x] Adds support enums for `ThrottleRequests::using` method by [@sethsandaru](https://github.com/sethsandaru) in https://github.com/laravel/framework/pull/57190 +* [12.x] Introduce "after" rate limiting by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/57125 +* [12.x] Json schema nullable by [@Katalam](https://github.com/Katalam) in https://github.com/laravel/framework/pull/57181 +* [12.x] Dispatch framework events on composer `pre-package-uninstall` event by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57144 +* [12.x] Add Http::batch by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/56946 +* [12.x] [Mail] Update `queue` PHPDoc according to function behavior by [@MrYamous](https://github.com/MrYamous) in https://github.com/laravel/framework/pull/57207 +* [12.x] Remove unnecessary parentheses by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57212 +* [12.x] Remove unnecessary parentheses by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57210 +* [12.x] Fixes error renderer report page by [@xiCO2k](https://github.com/xiCO2k) in https://github.com/laravel/framework/pull/57208 +* [12.x] Extend SQS FIFO and fair queue support by [@patrickcarlohickman](https://github.com/patrickcarlohickman) in https://github.com/laravel/framework/pull/57187 + +## [v12.31.1](https://github.com/laravel/framework/compare/v12.31.0...v12.31.1) - 2025-09-23 + +* Revert "[12.x] Reintroduce short-hand "false" syntax for Blade component props" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57151 + +## [v12.31.0](https://github.com/laravel/framework/compare/v12.30.1...v12.31.0) - 2025-09-23 + +* Bump vite from 7.1.2 to 7.1.6 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/57114 +* [12.x] Reintroduce short-hand "false" syntax for Blade component props by [@PerryvanderMeer](https://github.com/PerryvanderMeer) in https://github.com/laravel/framework/pull/57104 +* [12.x] Allow Number parse helpers to return false by [@platoindebugmode](https://github.com/platoindebugmode) in https://github.com/laravel/framework/pull/57127 +* [12.x] Refactor `RedisTaggedCache@flush()` to allow for custom connections by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/57122 +* [12.x] Use light-dark scheme for exception renderer by [@pxlrbt](https://github.com/pxlrbt) in https://github.com/laravel/framework/pull/57128 +* [12.x] Replace logger helper and log function concrete return type ?LogManager with abstract ?LoggerInterface by [@abdelrahmenAyman](https://github.com/abdelrahmenAyman) in https://github.com/laravel/framework/pull/57028 +* [12.x] Fix session value is missing assertion by [@barclaymichael](https://github.com/barclaymichael) in https://github.com/laravel/framework/pull/57134 +* median() div swapped for intdiv() by [@artumi-richard](https://github.com/artumi-richard) in https://github.com/laravel/framework/pull/57148 +* [12.x] Fix PHP 8.5 null-key deprecations by [@IonBazan](https://github.com/IonBazan) in https://github.com/laravel/framework/pull/57137 + +## [v12.30.1](https://github.com/laravel/framework/compare/v12.30.0...v12.30.1) - 2025-09-18 + +* [12.x] Fix: Apply intl extension check to ordinal position to prevent issues by [@BinaryKitten](https://github.com/BinaryKitten) in https://github.com/laravel/framework/pull/57112 + +## [v12.30.0](https://github.com/laravel/framework/compare/v12.29.0...v12.30.0) - 2025-09-18 + +* [12.x] Allow newer versions for phiki/phiki than 2.0.0 by [@hebbet](https://github.com/hebbet) in https://github.com/laravel/framework/pull/57075 +* [12.x] Use null coalescing for memoryExceededExitCode by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57090 +* [12.x] Fix 'can' function that was defined in RouterRegistrar in #54648 by [@pdewit](https://github.com/pdewit) in https://github.com/laravel/framework/pull/57072 +* [12.x] Fix SQS FIFO and fair queue support by [@patrickcarlohickman](https://github.com/patrickcarlohickman) in https://github.com/laravel/framework/pull/57080 +* atomically flush redis cache tags by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57098 +* [12.x] Add type hints to `\Illuminate\Support\Str` by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/57096 +* Doc: Update Database Connection getElapsedTime comment to specify unit by [@glensc](https://github.com/glensc) in https://github.com/laravel/framework/pull/57099 +* [12.x] Add support for Ordinal Position in validation messages by [@BinaryKitten](https://github.com/BinaryKitten) in https://github.com/laravel/framework/pull/57109 +* [12.x] Fix exception frame file path on Windows by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57103 +* Add fallback to copy buttons on new exception page by [@joaokamun](https://github.com/joaokamun) in https://github.com/laravel/framework/pull/57092 +* [12.x] Adds `Macroable` trait to `Illuminate/Support/Benchmark` by [@1tim22](https://github.com/1tim22) in https://github.com/laravel/framework/pull/57107 + +## [v12.29.0](https://github.com/laravel/framework/compare/v12.28.1...v12.29.0) - 2025-09-16 + +* Ensure cached and uncached routes share same precedence when resolving actions and names by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/56920 +* [12.x] Re-enable previously commented assertions by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56930 +* [12.x] Reorder .gitignore entries for consistency and readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56963 +* [12.x] SQLite: Allow setting any pragmas by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/56962 +* refactor: remove unused array from docblock by [@alipowerful7](https://github.com/alipowerful7) in https://github.com/laravel/framework/pull/56961 +* PendingResourceRegistration withoutMiddleware never returns array by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/56959 +* [12.x] Allow not having "fakerphp/faker" installed by [@SjorsO](https://github.com/SjorsO) in https://github.com/laravel/framework/pull/56953 +* [12.x] Fix Validator placeholderHash PHPDoc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56947 +* [12.x] Handle MariaDB innodb_snapshot_isolation=ON by [@Muffinman](https://github.com/Muffinman) in https://github.com/laravel/framework/pull/56945 +* [12.x] Add PhpRedis pack ignore numbers option by [@tuandp](https://github.com/tuandp) in https://github.com/laravel/framework/pull/56941 +* test(support): add edge-case tuples for preg_replace_array by [@realpvz](https://github.com/realpvz) in https://github.com/laravel/framework/pull/56937 +* [12.x] Allow for BackedEnum on dynamic blade component by [@gehrisandro](https://github.com/gehrisandro) in https://github.com/laravel/framework/pull/56940 +* [12.x] Remove one redundant array access by [@vincentvanhoven](https://github.com/vincentvanhoven) in https://github.com/laravel/framework/pull/56931 +* [12.x] Add withoutGlobalScopesExcept() to keep only specified global scopes by [@theHocineSaad](https://github.com/theHocineSaad) in https://github.com/laravel/framework/pull/56957 +* [12.x] Make visibility consistent by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56970 +* [12.x] Change list to tuple in PHPDoc block by [@shaedrich](https://github.com/shaedrich) in https://github.com/laravel/framework/pull/56967 +* [12.x] Improve `AggregateServiceProvider` docblocks by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56968 +* [12.x] add --whisper option to schedule:work command by [@thojo0](https://github.com/thojo0) in https://github.com/laravel/framework/pull/56969 +* [12.x] Update Faker suggestion to match skeleton version by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56974 +* Refactor: use str_contains() instead of strpos() for clarity by [@arshidkv12](https://github.com/arshidkv12) in https://github.com/laravel/framework/pull/56979 +* [12.x] remove unnecessary `with()` helper call by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56975 +* [12.x] Config: Move some items into pragmas by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56980 +* Add callback support to takeUntilTimeout in LazyCollection by [@kamilkozak](https://github.com/kamilkozak) in https://github.com/laravel/framework/pull/56981 +* [12.x] Utilize the is_finite() PHP function by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56990 +* [12.x] Use property promotion in `MessageLogged` and narrow `$level` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56989 +* [12.x] do not use `with()` helper when no second argument is passed by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56986 +* [12.x] Correct the type of $handler from Connection::whenQueryingForLongerThan by [@sethsandaru](https://github.com/sethsandaru) in https://github.com/laravel/framework/pull/56987 +* [12.x] Some quick fixes by [@theHocineSaad](https://github.com/theHocineSaad) in https://github.com/laravel/framework/pull/56991 +* tests: Ensure transaction callbacks run in FIFO order by [@realpvz](https://github.com/realpvz) in https://github.com/laravel/framework/pull/56973 +* Pass $attributes and $parent arguments to Factory Sequence by [@fritz-c](https://github.com/fritz-c) in https://github.com/laravel/framework/pull/56972 +* [12.x] - Support `Castable` on `Enum` by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/56977 +* [12.x] add trailing commas in multiline method signatures by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56992 +* [12.x] Improve docblocks for nullable parameters by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56995 +* [12.x] Improve docblocks for nullable parameters by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56996 +* [12.x] Improve docblocks for nullable parameters by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56997 +* Revert "[12.x] Config: Move some items into pragmas" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/57003 +* [12.x]: Cache Session Driver by [@joaopalopes24](https://github.com/joaopalopes24) in https://github.com/laravel/framework/pull/56887 +* [12.x] Add support for #[UseResource(...)] and #[UseResourceCollection(...)] attributes on models by [@Lukasss93](https://github.com/Lukasss93) in https://github.com/laravel/framework/pull/56966 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57010 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/57031 +* [12.x] Infinite method chaining in contextual binding builder by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57026 +* [12.x] Improved manager typehints by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/57024 +* Bump vite from 5.4.19 to 5.4.20 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/laravel/framework/pull/57009 +* [12.x] Correct APC cache store docblock types by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/57020 +* [12.x] Enable dynamic tries() method on Queueable Listeners by [@glioympas](https://github.com/glioympas) in https://github.com/laravel/framework/pull/57014 +* [12.x] Add --json option to ScheduleListCommand by [@dxnter](https://github.com/dxnter) in https://github.com/laravel/framework/pull/57006 +* [12.x] `with()` helper call simplification by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57041 +* [12.x] handle all Enum types for default values by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/57040 +* [12.x] Refactor chained method calls for readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57050 +* [12.x] Improve docblock wording by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57056 +* [12.x] Refactor chained method calls for readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/57054 +* [12.x] Update local exception page by [@avosalmon](https://github.com/avosalmon) in https://github.com/laravel/framework/pull/57036 +* [12.x] Add ability to control QueueWorker memory exceeded exit code by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/57044 +* [12.x] Ensure `laravel-cloud-socket` respects `LOG_LEVEL` by [@PeteBishwhip](https://github.com/PeteBishwhip) in https://github.com/laravel/framework/pull/57071 + +## [v12.28.1](https://github.com/laravel/framework/compare/v12.28.0...v12.28.1) - 2025-09-04 + +* [12.x] Rename `group` to `messageGroup` property by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56919 +* Fix PHP_CLI_SERVER_WORKERS inside laravel/sail by [@akyrey](https://github.com/akyrey) in https://github.com/laravel/framework/pull/56923 +* Allow RouteRegistrar to be Macroable by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/56921 +* [12.x] Fix SesV2Transport docblock by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/56917 +* [12.x] Prevent unnecessary query logging on exceptions with a custom renderer by [@luanfreitasdev](https://github.com/luanfreitasdev) in https://github.com/laravel/framework/pull/56874 +* [12.x] Reduce meaningless intermediate variables by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56927 + +## [v12.28.0](https://github.com/laravel/framework/compare/v12.27.1...v12.28.0) - 2025-09-03 + +* [11.x] Correct how base options for missing config files are preloaded by [@u01jmg3](https://github.com/u01jmg3) in https://github.com/laravel/framework/pull/56216 +* [11.x] backport #56235 by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56236 +* [11.x] Consistent use of `mb_split()` to split strings into words by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56617 +* [11.x] `CacheSchedulingMutex` should use lock connection by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56614 +* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56630 +* [11.x] Update `orchestra/testbench-core` deps by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56636 +* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56649 +* [11.x] Fix exception page not preparing SQL bindings by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56651 +* [11.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56849 +* [11.x] Chore: Decouple Str::random() from Validator by [@michaeldyrynda](https://github.com/michaeldyrynda) in https://github.com/laravel/framework/pull/56852 +* [11.x] Allow a wider range of `brick/math` versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56890 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56894 +* [12.x] Switch back to ternaries in `DatabaseManager` to allow for empty named connections by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56906 +* [12.x] Update config/database.php to match the latest skeleton configuration by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56905 +* Update fluent() helper by [@tanthammar](https://github.com/tanthammar) in https://github.com/laravel/framework/pull/56900 +* [12.x] Add method to retrieve the command on InvokedProcess by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/56886 +* [12.x] provide a default slot name when compiling by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56883 +* [12.x] Allow enums on model connection property and methods by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56896 +* [12.x] Adds internal class by [@nunomaduro](https://github.com/nunomaduro) in https://github.com/laravel/framework/pull/56903 + +## [v12.27.1](https://github.com/laravel/framework/compare/v12.27.0...v12.27.1) - 2025-09-02 + +* [12.x] Allow a wider range of `brick/math` versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56891 +* [12.x] Fix secure_url() breaking changes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56885 + +## [v12.27.0](https://github.com/laravel/framework/compare/v12.26.4...v12.27.0) - 2025-09-02 + +* [12.x] Add prepend option for Str::plural() by [@caseydwyer](https://github.com/caseydwyer) in https://github.com/laravel/framework/pull/56802 +* [12.x] Fix multi-line embedded image replacement in mail views by [@iammursal](https://github.com/iammursal) in https://github.com/laravel/framework/pull/56828 +* [12.x] Add supports for SQS Fair Queue by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56763 +* [12.x] Support enum values in `Collection` `countBy` method by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56830 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56838 +* [12.x] Fix docblocks and all() method in ArrayStore for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56845 +* [12.x] Improve Grammar in ArrayLock by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56844 +* [12.x] Normalize comments for timestampsTz() and nullableTimestampsTz() by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56840 +* [12.x] Reduce meaningless intermediate variables by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56843 +* [12.x] Simpler and consistent `Arr::collapse()` by [@weshooper](https://github.com/weshooper) in https://github.com/laravel/framework/pull/56842 +* [12.x] Improving readability by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56847 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56850 +* [12.x] Remove extra space before line number in exception trace by [@mtbossa](https://github.com/mtbossa) in https://github.com/laravel/framework/pull/56863 +* [12.x] Remove unused variable by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56861 +* [12.x] Add support for `UnitEnum` in `Collection` `groupBy` method by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56857 +* [12.x] Add missing void return type to test methods by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56860 +* [12.x] Improve `countBy` docblock in `Collection` to allow for enum callback by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56856 +* [12.x] Improve `InteractsWithContainer` return types by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/56853 +* [12.x] Allow mass assignment for value object casting. by [@AbdelElrafa](https://github.com/AbdelElrafa) in https://github.com/laravel/framework/pull/56871 +* [12.x] Allows `APP_BASE_PATH` from `$_SERVER` by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56868 +* [12.x] Fix typo in docblock by [@dwightwatson](https://github.com/dwightwatson) in https://github.com/laravel/framework/pull/56867 +* [12.x] Allow enums in other DatabaseManager methods by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56878 +* Add health score badge to README by [@jonathimer](https://github.com/jonathimer) in https://github.com/laravel/framework/pull/56875 +* [12.x] Let `toPrettyJson()` accepts options by [@lucasmichot](https://github.com/lucasmichot) in https://github.com/laravel/framework/pull/56876 + +## [v12.26.4](https://github.com/laravel/framework/compare/v12.26.3...v12.26.4) - 2025-08-29 + +* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56792 +* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56794 +* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56795 +* [12.x] Add support for nested array notation within `loadMissing` by [@angus-mcritchie](https://github.com/angus-mcritchie) in https://github.com/laravel/framework/pull/56711 +* [12.x] Colocate Container build functions with the `SelfBuilding` interface by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56731 +* perf: optimize loop performance by pre-calculating array counts in Str::apa() and fileSize() methods by [@AmadulHaque](https://github.com/AmadulHaque) in https://github.com/laravel/framework/pull/56796 +* fix: Helper function secure_url not always returning a string by [@SOD96](https://github.com/SOD96) in https://github.com/laravel/framework/pull/56807 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56803 +* [12.x] Parse Redis "friendly" algorithm names into integers by [@mateusjatenee](https://github.com/mateusjatenee) in https://github.com/laravel/framework/pull/56800 +* [12.x] Remove [@return](https://github.com/return) tags from constructors by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56814 +* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56813 +* [12.x] Use FQCN for [@mixin](https://github.com/mixin) annotation for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56811 +* [12.x] Remove leftover `method_exists` checks by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/56821 +* [12.x] Fix use array_first and array_last by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56820 +* Support enum in Collection -> keyBy() by [@zKoz210](https://github.com/zKoz210) in https://github.com/laravel/framework/pull/56786 +* Adds make:config command by [@inmanturbo](https://github.com/inmanturbo) in https://github.com/laravel/framework/pull/56819 + +## [v12.26.3](https://github.com/laravel/framework/compare/v12.26.2...v12.26.3) - 2025-08-27 + +* [12.x] add back return type by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56774 +* fix: base class guard in return types is breaking custom guards by [@phadaphunk](https://github.com/phadaphunk) in https://github.com/laravel/framework/pull/56779 +* [12.x] Standardise polyfill dependencies by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56781 +* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56790 +* [12.x] Refactor duplicated logic in ReplacesAttributes by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56789 +* [12.x] Improve output grammar in `ScheduleRunCommand` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56776 + +## [v12.26.2](https://github.com/laravel/framework/compare/v12.26.1...v12.26.2) - 2025-08-26 + +* [12.x] fix: csrf_token can return null by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56768 +* [12.x] Fix `date_format` validation on DST Timezone by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56767 +* [12.x] Fix event helper by [@jasonvarga](https://github.com/jasonvarga) in https://github.com/laravel/framework/pull/56773 + +## [v12.26.1](https://github.com/laravel/framework/compare/v12.26.0...v12.26.1) - 2025-08-26 + +* [12.x] fix: add polyfill requirement to illuminate packages by [@erikgaal](https://github.com/erikgaal) in https://github.com/laravel/framework/pull/56765 +* [12.x] revert changes to `old()` helper by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/56769 + +## [v12.26.0](https://github.com/laravel/framework/compare/v12.25.0...v12.26.0) - 2025-08-26 + +* [12.x] feat: add native return types to helper functions by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/56684 +* [12.x] Allow passing enum to `Database` attribute by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56688 +* [12.x] Clean up redundant type hints in docblocks by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56690 +* Add ability to specify a transaction mode for SQLite connection by [@panda-madness](https://github.com/panda-madness) in https://github.com/laravel/framework/pull/56681 +* [12.x] Fix `spliceIntoPosition` docblock to allow `string|int` values by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56698 +* [12.x] Use array_first and array_last polyfills by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/56703 +* [12.x] Fix path to Str in exception markdown by [@apreiml](https://github.com/apreiml) in https://github.com/laravel/framework/pull/56705 +* [12.x] Add `withHeartbeat` method to `LazyCollection` by [@JosephSilber](https://github.com/JosephSilber) in https://github.com/laravel/framework/pull/56477 +* [12.x] Add toPrettyJson method by [@WendellAdriel](https://github.com/WendellAdriel) in https://github.com/laravel/framework/pull/56697 +* [12.x] Use `array_first` and `array_last` by [@KIKOmanasijev](https://github.com/KIKOmanasijev) in https://github.com/laravel/framework/pull/56706 +* [12.x] Do not dispatch `MessageLogged` twice by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56713 +* [12.x] Order classes alphabetically by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56743 +* [12.x] Normalize file path separators for commands on Windows by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56734 +* [12.x] Improve `queue:prune-failed` tests coverage by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56732 +* [12.x] Align trait usage for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56727 +* [12.x] Fix composer suggests for illuminate/container by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56722 +* Add nullableTimestampsTz method to Blueprint by [@mohamedhabibwork](https://github.com/mohamedhabibwork) in https://github.com/laravel/framework/pull/56720 +* Add possibility to override symbol when using currency format by [@PhilippeThouvenot](https://github.com/PhilippeThouvenot) in https://github.com/laravel/framework/pull/56749 +* [12.x] Revert #56608 by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56752 +* Revert "Add possibility to override symbol when using currency format" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/56753 +* [12.x] Support `null` parameter in `BusFake::chain()` method by [@stevebauman](https://github.com/stevebauman) in https://github.com/laravel/framework/pull/56750 +* [12.x] Remove unnecessary return in ddBody for consistency by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56759 +* [12.x] Make interface accept UnitEnum by [@parijke](https://github.com/parijke) in https://github.com/laravel/framework/pull/56758 +* [12.x] Fix concurrency closure invocation: use base64 encoding by [@sashko-guz](https://github.com/sashko-guz) in https://github.com/laravel/framework/pull/56757 +* [12.x] `ArrayStore::all()` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56751 +* [12.x] Fix: Add `$forceWrap` property to JsonResource for consistent API response #56724 by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/56736 +* [12.x] Ensures casts objects can be transformed into strings by [@DarkGhostHunter](https://github.com/DarkGhostHunter) in https://github.com/laravel/framework/pull/56687 + +## [v12.25.0](https://github.com/laravel/framework/compare/v12.24.0...v12.25.0) - 2025-08-18 + +* [12.x] Prioritize Current Schema When Resolving the Table Name in `db:table` Command by [@hafezdivandari](https://github.com/hafezdivandari) in https://github.com/laravel/framework/pull/56646 +* [12.x] Add `allowedUrls` through `preventStrayRequests` by [@rabrowne85](https://github.com/rabrowne85) in https://github.com/laravel/framework/pull/56645 +* [12.x] Add "Copy as Markdown" button to error page by [@mpociot](https://github.com/mpociot) in https://github.com/laravel/framework/pull/56657 +* [12.x] Indicate that `Context@scope()` may throw by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56655 +* [12.x] Remove [@throws](https://github.com/throws) phpDocs in the TransformToResource trait by [@adelf](https://github.com/adelf) in https://github.com/laravel/framework/pull/56667 +* [12.x] Improve docblocks for InteractsWithDatabase by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56666 +* [12.x] Fix prevent group attribute pollution in schedule by [@People-Sea](https://github.com/People-Sea) in https://github.com/laravel/framework/pull/56677 +* Add new `mergeVisible`, `mergeHidden` and `mergeAppends` methods. by [@jonerickson](https://github.com/jonerickson) in https://github.com/laravel/framework/pull/56678 + +## [v12.24.0](https://github.com/laravel/framework/compare/v12.23.1...v12.24.0) - 2025-08-13 + +* [8.4] Use PHP 8.4 array helpers in Arr utils by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56631 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56635 +* [12.x] Update `orchestra/testbench-core` deps by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56637 +* refactor: update cid param name by [@cpenned](https://github.com/cpenned) in https://github.com/laravel/framework/pull/56634 +* [12.x] Cache Singleton/Scoped attribute checks by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56633 +* [12.x] Add `Arr::push()` by [@inxilpro](https://github.com/inxilpro) in https://github.com/laravel/framework/pull/56632 +* [12.x] Add error message for `doesnt_contain` rule by [@apih](https://github.com/apih) in https://github.com/laravel/framework/pull/56644 + +## [v12.23.1](https://github.com/laravel/framework/compare/v12.23.0...v12.23.1) - 2025-08-12 + +## [v12.23.0](https://github.com/laravel/framework/compare/v12.22.1...v12.23.0) - 2025-08-12 + +* [12.x] Prevent unintended sleep on early failure of assertSequence by [@xHeaven](https://github.com/xHeaven) in https://github.com/laravel/framework/pull/56583 +* [12.x] Redis cluster broadcaster by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/56581 +* [12.x] Alias Benchmark class by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/56594 +* [12.x] Add support for drop patterns to the `make:migration` command's `TableGuesser`. by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56608 +* [12.x] Improve collection return types by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/56599 +* [12.x] Fix collection typo by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/56597 +* Fix return type docblock for resetAttempts method in RateLimiter by [@jonagoldman](https://github.com/jonagoldman) in https://github.com/laravel/framework/pull/56596 +* Add 'page' field to paginator links by [@compico](https://github.com/compico) in https://github.com/laravel/framework/pull/56603 +* [12.x] Add support for inline attachments in Resend transport by [@jayanratna](https://github.com/jayanratna) in https://github.com/laravel/framework/pull/56598 +* Fix test failures in PHPUnit 12.3.2 by [@KentarouTakeda](https://github.com/KentarouTakeda) in https://github.com/laravel/framework/pull/56610 +* [12.x] Use new error and exception handler getters by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56623 +* [12.x] Use PHP 8.4 array helpers by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56619 +* [12.x] Prefer Symfony PHP polyfills over `function_exists` calls by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56621 +* [12.x] `Bind` attribute accepts UnitEnum by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56616 +* [12.x] Add Vitess-specific safe to retry errors by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/56615 +* [12.x] Handle null as a falsy condition by [@negoziator](https://github.com/negoziator) in https://github.com/laravel/framework/pull/56612 +* Added "after" support for morphs and nullableMorphs Blueprint by [@marcogermani87](https://github.com/marcogermani87) in https://github.com/laravel/framework/pull/56613 +* [12.x] Fix usage of `Scoped` and `Singleton` on interfaces by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56620 +* [12.x] Online (concurrently) index creation for PostgreSQL and SqlServer by [@vadimonus](https://github.com/vadimonus) in https://github.com/laravel/framework/pull/56625 + +## [v12.22.1](https://github.com/laravel/framework/compare/v12.21.0...v12.22.1) - 2025-08-08 + +* [12.x] Improved assertion message by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/56579 +* [12.x] Fixed version increment by [@dciancu](https://github.com/dciancu) in https://github.com/laravel/framework/pull/56588 +* [12.x] Normalize file path separators in `make:migration` command on Windows by [@amirhshokri](https://github.com/amirhshokri) in https://github.com/laravel/framework/pull/56591 +* Revert "[12.x] Improve PHPDoc blocks for array of arguments in Gate" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/56593 + +## [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 + +* [12.x] Support `useCurrent` on date and year column types by [@nicholasbrantley](https://github.com/nicholasbrantley) in https://github.com/laravel/framework/pull/55619 +* [12.x] Update "Number::fileSize" to use correct prefix and add prefix param by [@Boy132](https://github.com/Boy132) in https://github.com/laravel/framework/pull/55678 +* [12.x] Update PHPDoc for whereRaw to allow Expression as $sql by [@mitoop](https://github.com/mitoop) in https://github.com/laravel/framework/pull/55674 +* Revert "[12.x] Make Blueprint Resolver Statically" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55690 +* [12.x] Support Virtual Properties When Serializing Models by [@beschoenen](https://github.com/beschoenen) in https://github.com/laravel/framework/pull/55691 +* [12.X] Fix `Http::preventStrayRequests` error propagation when using `Http::pool` by [@LeTamanoir](https://github.com/LeTamanoir) in https://github.com/laravel/framework/pull/55689 +* [12.x] incorrect use of generics in Schema\Builder by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55687 +* [12.x] Add option to disable MySQL ssl when restoring or squashing migrations by [@andersonls](https://github.com/andersonls) in https://github.com/laravel/framework/pull/55683 +* [12.x] Add `except` and `exceptHidden` methods to `Context` class by [@xurshudyan](https://github.com/xurshudyan) in https://github.com/laravel/framework/pull/55692 +* [12.x] Container `currentlyResolving` utility by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/55684 +* [12.x] Container `currentlyResolving` test by [@jrseliga](https://github.com/jrseliga) in https://github.com/laravel/framework/pull/55694 +* [12.x] Fix handling of default values for route parameters with a binding field by [@stancl](https://github.com/stancl) in https://github.com/laravel/framework/pull/55697 +* Move Timebox for Authentication and add to password resets by [@valorin](https://github.com/valorin) in https://github.com/laravel/framework/pull/55701 +* [12.x] perf: Optimize BladeCompiler by [@rzv-me](https://github.com/rzv-me) in https://github.com/laravel/framework/pull/55703 +* [12.x] perf: support iterables for event discovery paths by [@calebdw](https://github.com/calebdw) in https://github.com/laravel/framework/pull/55699 +* [12.x] Types: AuthorizesRequests::resourceAbilityMap by [@liamduckett](https://github.com/liamduckett) in https://github.com/laravel/framework/pull/55706 +* [12.x] Add flexible support to memoized cache store by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55709 +* [12.x] Introduce Arr::from() by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55715 +* [12.x] Fix the `getCurrentlyAttachedPivots` wrong `morphClass` for morph to many relationships by [@amir9480](https://github.com/amir9480) in https://github.com/laravel/framework/pull/55721 +* [12.x] Improve typehints for Http classes by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/54783 +* Add deleteWhen for throttle exceptions job middleware by [@moshe-autoleadstar](https://github.com/moshe-autoleadstar) in https://github.com/laravel/framework/pull/55718 + +## [v12.13.0](https://github.com/laravel/framework/compare/v12.12.0...v12.13.0) - 2025-05-07 + +* [12.x] fix no arguments return type in request class by [@olivernybroe](https://github.com/olivernybroe) in https://github.com/laravel/framework/pull/55631 +* [12.x] Add support for callback evaluation in containsOneItem method by [@fernandokbs](https://github.com/fernandokbs) in https://github.com/laravel/framework/pull/55622 +* [12.x] add generics to aggregate related methods and properties by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55628 +* [12.x] Fix typo in PHPDoc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55636 +* [12.x] Allow naming queued closures by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55634 +* [12.x] Add `assertRedirectBack` assertion method by [@ryangjchandler](https://github.com/ryangjchandler) in https://github.com/laravel/framework/pull/55635 +* [12.x] Typehints for bindings by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55633 +* [12.x] add PHP Doc types to arrays for methods in Database\Grammar by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55629 +* fix trim null arg deprecation by [@apreiml](https://github.com/apreiml) in https://github.com/laravel/framework/pull/55649 +* [12.x] Support predis/predis 3.x by [@gabrielrbarbosa](https://github.com/gabrielrbarbosa) in https://github.com/laravel/framework/pull/55641 +* Bump vite from 5.4.18 to 5.4.19 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55655 +* [12.x] Fix predis versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/55654 +* [12.x] Bump minimum league/commonmark by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55659 +* [12.x] Fix typo in MemoizedStoreTest by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55662 +* [12.x] Queue event listeners with enum values by [@wgriffioen](https://github.com/wgriffioen) in https://github.com/laravel/framework/pull/55656 +* [12.x] Implement releaseAfter method in RateLimited middleware by [@adamjgriffith](https://github.com/adamjgriffith) in https://github.com/laravel/framework/pull/55671 +* [12.x] Improve Cache Tests by [@nuernbergerA](https://github.com/nuernbergerA) in https://github.com/laravel/framework/pull/55670 +* [12.x] Only pass model IDs to Eloquent `whereAttachedTo` method by [@ashleyshenton](https://github.com/ashleyshenton) in https://github.com/laravel/framework/pull/55666 +* feat(bus): allow adding multiple jobs to chain by [@dallyger](https://github.com/dallyger) in https://github.com/laravel/framework/pull/55668 +* [12.x] add generics to QueryBuilder’s column related methods by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55663 + +## [v12.12.0](https://github.com/laravel/framework/compare/v12.11.1...v12.12.0) - 2025-05-01 + +* [12.x] Make Blueprint Resolver Statically by [@finagin](https://github.com/finagin) in https://github.com/laravel/framework/pull/55607 +* [12.x] Allow limiting number of assets to preload by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55618 +* [12.x] Set job instance on "failed" command instance by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55617 + +## [v12.11.1](https://github.com/laravel/framework/compare/v12.11.0...v12.11.1) - 2025-04-30 + +* Revert "[12.x]`ScheduledTaskFailed` not dispatched on scheduled task failing" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55612 +* [12.x] Resolve issue with BelongsToManyRelationship factory by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/55608 + +## [v12.11.0](https://github.com/laravel/framework/compare/v12.10.2...v12.11.0) - 2025-04-29 + +* Add payload creation and original delay info to job payload by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55529 +* Add config option to ignore view cache timestamps by [@pizkaz](https://github.com/pizkaz) in https://github.com/laravel/framework/pull/55536 +* [12.x] Dispatch NotificationFailed when sending fails by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55507 +* [12.x] Option to disable dispatchAfterResponse in a test by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55456 +* [12.x] Pass flags to custom Json::$encoder by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55548 +* [12.x] Use pendingAttributes of relationships when creating relationship models via model factories by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55558 +* [12.x] Fix double query in model relation serialization by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55547 +* [12.x] Improve circular relation check in Automatic Relation Loading by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55542 +* [12.x] Prevent relation autoload context from being serialized by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55582 +* Remove `@internal` Annotation from `$components` Property in `InteractsWithIO` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55580 +* Ensure fake job implements job contract by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55574 +* [12.x] Fix `AnyOf` constructor parameter type by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/55577 +* Sync changes to Illuminate components before release by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/55591 +* [12.x] Set class-string generics on `Enum` rule by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55588 +* [12.x] added detailed doc types to bindings related methods by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55576 +* [12.x] Improve [@use](https://github.com/use) directive to support function and const modifiers by [@rodolfosrg](https://github.com/rodolfosrg) in https://github.com/laravel/framework/pull/55583 +* 12.x scheduled task failed not dispatched on scheduled task failing by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55572 +* [12.x] Introduce Reflector methods for accessing class attributes by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55568 +* [12.x] Typed getters for Arr helper by [@tibbsa](https://github.com/tibbsa) in https://github.com/laravel/framework/pull/55567 ## [v12.10.2](https://github.com/laravel/framework/compare/v12.10.1...v12.10.2) - 2025-04-24 diff --git a/README.md b/README.md index a6fb7790a95a..d3f84556ac4e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Total Downloads Latest Stable Version License +Health score

## About Laravel diff --git a/bin/release.sh b/bin/release.sh index 7e2aa75df1b9..98ec22aeaebf 100755 --- a/bin/release.sh +++ b/bin/release.sh @@ -52,7 +52,7 @@ git tag $VERSION git push origin --tags # Tag Components -for REMOTE in auth broadcasting bus cache collections conditionable config console container contracts cookie database encryption events filesystem hashing http log macroable mail notifications pagination pipeline process queue redis routing session support testing translation validation view +for REMOTE in auth broadcasting bus cache collections conditionable config console container contracts cookie database encryption events filesystem hashing http json-schema log macroable mail notifications pagination pipeline process queue redis routing session support testing translation validation view do echo "" echo "" diff --git a/bin/split.sh b/bin/split.sh index ae4cdf01f8f3..91fd373dbf0a 100755 --- a/bin/split.sh +++ b/bin/split.sh @@ -35,6 +35,7 @@ remote events git@github.com:illuminate/events.git remote filesystem git@github.com:illuminate/filesystem.git remote hashing git@github.com:illuminate/hashing.git remote http git@github.com:illuminate/http.git +remote json-schema git@github.com:illuminate/json-schema.git remote log git@github.com:illuminate/log.git remote macroable git@github.com:illuminate/macroable.git remote mail git@github.com:illuminate/mail.git @@ -69,6 +70,7 @@ split 'src/Illuminate/Events' events split 'src/Illuminate/Filesystem' filesystem split 'src/Illuminate/Hashing' hashing split 'src/Illuminate/Http' http +split 'src/Illuminate/JsonSchema' json-schema split 'src/Illuminate/Log' log split 'src/Illuminate/Macroable' macroable split 'src/Illuminate/Mail' mail diff --git a/composer.json b/composer.json index 708b0c12c893..3cd0043c01e2 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|^0.14", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", "egulias/email-validator": "^3.2.1|^4.0", @@ -33,7 +33,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.6", + "league/commonmark": "^2.7", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -51,7 +51,9 @@ "symfony/http-kernel": "^7.2.0", "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -79,6 +81,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", "illuminate/mail": "self.version", @@ -111,12 +114,13 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.0.0", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.7.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "predis/predis": "^2.3", + "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", @@ -177,7 +181,7 @@ "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", @@ -189,7 +193,7 @@ "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.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..4c754591211c 100644 --- a/config/cache.php +++ b/config/cache.php @@ -38,6 +38,11 @@ 'serialize' => false, ], + 'session' => [ + 'driver' => 'session', + 'key' => env('SESSION_CACHE_KEY', '_cache'), + ], + 'database' => [ 'driver' => 'database', 'connection' => env('DB_CACHE_CONNECTION'), @@ -103,6 +108,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..1d9883833e62 100644 --- a/config/database.php +++ b/config/database.php @@ -41,6 +41,8 @@ 'busy_timeout' => null, 'journal_mode' => null, 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + 'pragmas' => [], ], 'mysql' => [ @@ -148,7 +150,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), ], @@ -159,6 +161,10 @@ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], 'cache' => [ @@ -168,6 +174,10 @@ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], ], 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..2341efd8109d 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; @@ -185,7 +186,7 @@ protected function authorizeOnDemand($condition, $message, $code, $allowWhenResp $response = $condition; } - return with($response instanceof Response ? $response : new Response( + return ($response instanceof Response ? $response : new Response( (bool) $response === $allowWhenResponseIs, $message, $code ))->authorize(); } @@ -325,7 +326,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 mixed $arguments * @return bool */ public function allows($ability, $arguments = []) @@ -337,7 +338,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 mixed $arguments * @return bool */ public function denies($ability, $arguments = []) @@ -349,7 +350,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 mixed $arguments * @return bool */ public function check($abilities, $arguments = []) @@ -363,7 +364,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 mixed $arguments * @return bool */ public function any($abilities, $arguments = []) @@ -375,7 +376,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 mixed $arguments * @return bool */ public function none($abilities, $arguments = []) @@ -387,7 +388,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 mixed $arguments * @return \Illuminate\Auth\Access\Response * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -401,7 +402,7 @@ public function authorize($ability, $arguments = []) * Inspect the user for the given ability. * * @param \UnitEnum|string $ability - * @param array|mixed $arguments + * @param mixed $arguments * @return \Illuminate\Auth\Access\Response */ public function inspect($ability, $arguments = []) @@ -425,7 +426,7 @@ public function inspect($ability, $arguments = []) * Get the raw result from the authorization callback. * * @param string $ability - * @param array|mixed $arguments + * @param mixed $arguments * @return mixed * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -669,6 +670,12 @@ public function getPolicyFor($class) return $this->resolvePolicy($this->policies[$class]); } + $policy = $this->getPolicyFromAttribute($class); + + if (! is_null($policy)) { + return $this->resolvePolicy($policy); + } + foreach ($this->guessPolicyName($class) as $guessedPolicy) { if (class_exists($guessedPolicy)) { return $this->resolvePolicy($guessedPolicy); @@ -682,6 +689,25 @@ public function getPolicyFor($class) } } + /** + * Get the policy class from the class attribute. + * + * @param class-string<*> $class + * @return class-string<*>|null + */ + protected function getPolicyFromAttribute(string $class): ?string + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new ReflectionClass($class))->getAttributes(UsePolicy::class); + + return $attributes !== [] + ? $attributes[0]->newInstance()->class + : null; + } + /** * Guess the policy name for the given class. * diff --git a/src/Illuminate/Auth/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 70723558886e..3710d9b1941e 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); } /** @@ -126,22 +126,17 @@ public function createSessionDriver($name, $config) $this->createUserProvider($config['provider'] ?? null), $this->app['session.store'], rehashOnLogin: $this->app['config']->get('hashing.rehash_on_login', true), + timeboxDuration: $this->app['config']->get('auth.timebox_duration', 200000), ); // When using the remember me functionality of the authentication services we // will need to be set the encryption instance of the guard, which allows // secure, encrypted cookie values to get generated for those cookies. - if (method_exists($guard, 'setCookieJar')) { - $guard->setCookieJar($this->app['cookie']); - } + $guard->setCookieJar($this->app['cookie']); - if (method_exists($guard, 'setDispatcher')) { - $guard->setDispatcher($this->app['events']); - } + $guard->setDispatcher($this->app['events']); - if (method_exists($guard, 'setRequest')) { - $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); - } + $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); if (isset($config['remember'])) { $guard->setRememberDuration($config['remember']); 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/PasswordBroker.php b/src/Illuminate/Auth/Passwords/PasswordBroker.php index 89565eb77b3a..d955f3e42e1d 100755 --- a/src/Illuminate/Auth/Passwords/PasswordBroker.php +++ b/src/Illuminate/Auth/Passwords/PasswordBroker.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Arr; +use Illuminate\Support\Timebox; use UnexpectedValueException; class PasswordBroker implements PasswordBrokerContract @@ -34,18 +35,41 @@ class PasswordBroker implements PasswordBrokerContract */ protected $events; + /** + * The timebox instance. + * + * @var \Illuminate\Support\Timebox + */ + protected $timebox; + + /** + * The number of microseconds that the timebox should wait for. + * + * @var int + */ + protected $timeboxDuration; + /** * Create a new password broker instance. * * @param \Illuminate\Auth\Passwords\TokenRepositoryInterface $tokens * @param \Illuminate\Contracts\Auth\UserProvider $users * @param \Illuminate\Contracts\Events\Dispatcher|null $dispatcher + * @param \Illuminate\Support\Timebox|null $timebox + * @param int $timeboxDuration */ - public function __construct(#[\SensitiveParameter] TokenRepositoryInterface $tokens, UserProvider $users, ?Dispatcher $dispatcher = null) - { + public function __construct( + #[\SensitiveParameter] TokenRepositoryInterface $tokens, + UserProvider $users, + ?Dispatcher $dispatcher = null, + ?Timebox $timebox = null, + int $timeboxDuration = 200000, + ) { $this->users = $users; $this->tokens = $tokens; $this->events = $dispatcher; + $this->timebox = $timebox ?: new Timebox; + $this->timeboxDuration = $timeboxDuration; } /** @@ -57,33 +81,35 @@ public function __construct(#[\SensitiveParameter] TokenRepositoryInterface $tok */ public function sendResetLink(#[\SensitiveParameter] array $credentials, ?Closure $callback = null) { - // First we will check to see if we found a user at the given credentials and - // if we did not we will redirect back to this current URI with a piece of - // "flash" data in the session to indicate to the developers the errors. - $user = $this->getUser($credentials); + return $this->timebox->call(function () use ($credentials, $callback) { + // First we will check to see if we found a user at the given credentials and + // if we did not we will redirect back to this current URI with a piece of + // "flash" data in the session to indicate to the developers the errors. + $user = $this->getUser($credentials); - if (is_null($user)) { - return static::INVALID_USER; - } + if (is_null($user)) { + return static::INVALID_USER; + } - if ($this->tokens->recentlyCreatedToken($user)) { - return static::RESET_THROTTLED; - } + if ($this->tokens->recentlyCreatedToken($user)) { + return static::RESET_THROTTLED; + } - $token = $this->tokens->create($user); + $token = $this->tokens->create($user); - if ($callback) { - return $callback($user, $token) ?? static::RESET_LINK_SENT; - } + if ($callback) { + return $callback($user, $token) ?? static::RESET_LINK_SENT; + } - // Once we have the reset token, we are ready to send the message out to this - // user with a link to reset their password. We will then redirect back to - // the current URI having nothing set in the session to indicate errors. - $user->sendPasswordResetNotification($token); + // Once we have the reset token, we are ready to send the message out to this + // user with a link to reset their password. We will then redirect back to + // the current URI having nothing set in the session to indicate errors. + $user->sendPasswordResetNotification($token); - $this->events?->dispatch(new PasswordResetLinkSent($user)); + $this->events?->dispatch(new PasswordResetLinkSent($user)); - return static::RESET_LINK_SENT; + return static::RESET_LINK_SENT; + }, $this->timeboxDuration); } /** @@ -95,25 +121,29 @@ public function sendResetLink(#[\SensitiveParameter] array $credentials, ?Closur */ public function reset(#[\SensitiveParameter] array $credentials, Closure $callback) { - $user = $this->validateReset($credentials); + return $this->timebox->call(function ($timebox) use ($credentials, $callback) { + $user = $this->validateReset($credentials); - // If the responses from the validate method is not a user instance, we will - // assume that it is a redirect and simply return it from this method and - // the user is properly redirected having an error message on the post. - if (! $user instanceof CanResetPasswordContract) { - return $user; - } + // If the responses from the validate method is not a user instance, we will + // assume that it is a redirect and simply return it from this method and + // the user is properly redirected having an error message on the post. + if (! $user instanceof CanResetPasswordContract) { + return $user; + } - $password = $credentials['password']; + $password = $credentials['password']; - // Once the reset has been validated, we'll call the given callback with the - // new password. This gives the user an opportunity to store the password - // in their persistent storage. Then we'll delete the token and return. - $callback($user, $password); + // Once the reset has been validated, we'll call the given callback with the + // new password. This gives the user an opportunity to store the password + // in their persistent storage. Then we'll delete the token and return. + $callback($user, $password); - $this->tokens->delete($user); + $this->tokens->delete($user); + + $timebox->returnEarly(); - return static::PASSWORD_RESET; + return static::PASSWORD_RESET; + }, $this->timeboxDuration); } /** @@ -199,4 +229,14 @@ public function getRepository() { return $this->tokens; } + + /** + * Get the timebox instance used by the guard. + * + * @return \Illuminate\Support\Timebox + */ + public function getTimebox() + { + return $this->timebox; + } } diff --git a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php index 516638b17f5f..6e42bba190d8 100644 --- a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php +++ b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php @@ -70,6 +70,7 @@ protected function resolve($name) $this->createTokenRepository($config), $this->app['auth']->createUserProvider($config['provider'] ?? null), $this->app['events'] ?? null, + timeboxDuration: $this->app['config']->get('auth.timebox_duration', 200000), ); } @@ -94,7 +95,6 @@ protected function createTokenRepository(array $config) $key, ($config['expire'] ?? 60) * 60, $config['throttle'] ?? 0, - $config['prefix'] ?? '', ); } diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 13bd15f46c5a..8b388af1b9e5 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -96,6 +96,13 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth */ protected $timebox; + /** + * The number of microseconds that the timebox should wait for. + * + * @var int + */ + protected $timeboxDuration; + /** * Indicates if passwords should be rehashed on login if needed. * @@ -126,6 +133,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth * @param \Symfony\Component\HttpFoundation\Request|null $request * @param \Illuminate\Support\Timebox|null $timebox * @param bool $rehashOnLogin + * @param int $timeboxDuration */ public function __construct( $name, @@ -134,6 +142,7 @@ public function __construct( ?Request $request = null, ?Timebox $timebox = null, bool $rehashOnLogin = true, + int $timeboxDuration = 200000, ) { $this->name = $name; $this->session = $session; @@ -141,6 +150,7 @@ public function __construct( $this->provider = $provider; $this->timebox = $timebox ?: new Timebox; $this->rehashOnLogin = $rehashOnLogin; + $this->timeboxDuration = $timeboxDuration; } /** @@ -290,9 +300,17 @@ public function onceUsingId($id) */ public function validate(array $credentials = []) { - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + return $this->timebox->call(function ($timebox) use ($credentials) { + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - return $this->hasValidCredentials($user, $credentials); + $validated = $this->hasValidCredentials($user, $credentials); + + if ($validated) { + $timebox->returnEarly(); + } + + return $validated; + }, $this->timeboxDuration); } /** @@ -390,27 +408,31 @@ protected function failedBasicResponse() */ public function attempt(array $credentials = [], $remember = false) { - $this->fireAttemptEvent($credentials, $remember); + return $this->timebox->call(function ($timebox) use ($credentials, $remember) { + $this->fireAttemptEvent($credentials, $remember); - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - // If an implementation of UserInterface was returned, we'll ask the provider - // to validate the user against the given credentials, and if they are in - // fact valid we'll log the users into the application and return true. - if ($this->hasValidCredentials($user, $credentials)) { - $this->rehashPasswordIfRequired($user, $credentials); + // If an implementation of UserInterface was returned, we'll ask the provider + // to validate the user against the given credentials, and if they are in + // fact valid we'll log the users into the application and return true. + if ($this->hasValidCredentials($user, $credentials)) { + $this->rehashPasswordIfRequired($user, $credentials); - $this->login($user, $remember); + $this->login($user, $remember); - return true; - } + $timebox->returnEarly(); - // If the authentication attempt fails we will fire an event so that the user - // may be notified of any suspicious attempts to access their account from - // an unrecognized user. A developer may listen to this event as needed. - $this->fireFailedEvent($user, $credentials); + return true; + } - return false; + // If the authentication attempt fails we will fire an event so that the user + // may be notified of any suspicious attempts to access their account from + // an unrecognized user. A developer may listen to this event as needed. + $this->fireFailedEvent($user, $credentials); + + return false; + }, $this->timeboxDuration); } /** @@ -423,24 +445,28 @@ public function attempt(array $credentials = [], $remember = false) */ public function attemptWhen(array $credentials = [], $callbacks = null, $remember = false) { - $this->fireAttemptEvent($credentials, $remember); + return $this->timebox->call(function ($timebox) use ($credentials, $callbacks, $remember) { + $this->fireAttemptEvent($credentials, $remember); - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - // This method does the exact same thing as attempt, but also executes callbacks after - // the user is retrieved and validated. If one of the callbacks returns falsy we do - // not login the user. Instead, we will fail the specific authentication attempt. - if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) { - $this->rehashPasswordIfRequired($user, $credentials); + // This method does the exact same thing as attempt, but also executes callbacks after + // the user is retrieved and validated. If one of the callbacks returns falsy we do + // not login the user. Instead, we will fail the specific authentication attempt. + if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) { + $this->rehashPasswordIfRequired($user, $credentials); - $this->login($user, $remember); + $this->login($user, $remember); - return true; - } + $timebox->returnEarly(); - $this->fireFailedEvent($user, $credentials); + return true; + } - return false; + $this->fireFailedEvent($user, $credentials); + + return false; + }, $this->timeboxDuration); } /** @@ -452,17 +478,13 @@ public function attemptWhen(array $credentials = [], $callbacks = null, $remembe */ protected function hasValidCredentials($user, $credentials) { - return $this->timebox->call(function ($timebox) use ($user, $credentials) { - $validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials); - - if ($validated) { - $timebox->returnEarly(); + $validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials); - $this->fireValidatedEvent($user); - } + if ($validated) { + $this->fireValidatedEvent($user); + } - return $validated; - }, 200 * 1000); + return $validated; } /** @@ -544,7 +566,7 @@ public function login(AuthenticatableContract $user, $remember = false) } /** - * Update the session with the given ID. + * Update the session with the given ID and regenerate the session's token. * * @param string $id * @return void @@ -553,7 +575,7 @@ protected function updateSession($id) { $this->session->put($this->getName(), $id); - $this->session->migrate(true); + $this->session->regenerate(true); } /** diff --git a/src/Illuminate/Broadcasting/BroadcastEvent.php b/src/Illuminate/Broadcasting/BroadcastEvent.php index c4da0faab220..2ef568dc6bb0 100644 --- a/src/Illuminate/Broadcasting/BroadcastEvent.php +++ b/src/Illuminate/Broadcasting/BroadcastEvent.php @@ -141,13 +141,13 @@ protected function formatProperty($value) * Get the channels for the given connection. * * @param array $channels - * @param string $connection + * @param string|null $connection * @return array */ protected function getConnectionChannels($channels, $connection) { - return is_array($channels[$connection] ?? null) - ? $channels[$connection] + return is_array($channels[$connection ?? ''] ?? null) + ? $channels[$connection ?? ''] : $channels; } @@ -155,13 +155,13 @@ protected function getConnectionChannels($channels, $connection) * Get the payload for the given connection. * * @param array $payload - * @param string $connection + * @param string|null $connection * @return array */ protected function getConnectionPayload($payload, $connection) { - $connectionPayload = is_array($payload[$connection] ?? null) - ? $payload[$connection] + $connectionPayload = is_array($payload[$connection ?? ''] ?? null) + ? $payload[$connection ?? ''] : $payload; if (isset($payload['socket'])) { @@ -188,7 +188,7 @@ public function middleware(): array /** * Handle a job failure. * - * @param \Throwable $e + * @param \Throwable|null $e * @return void */ public function failed(?Throwable $e = null): void diff --git a/src/Illuminate/Broadcasting/BroadcastManager.php b/src/Illuminate/Broadcasting/BroadcastManager.php index 790e096bbaa2..acb01d7bd31d 100644 --- a/src/Illuminate/Broadcasting/BroadcastManager.php +++ b/src/Illuminate/Broadcasting/BroadcastManager.php @@ -14,12 +14,15 @@ 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; use InvalidArgumentException; use Psr\Log\LoggerInterface; use Pusher\Pusher; +use RuntimeException; +use Throwable; /** * @mixin \Illuminate\Contracts\Broadcasting\Broadcaster @@ -158,7 +161,7 @@ public function presence(string $channel): AnonymousEvent /** * Begin broadcasting an event. * - * @param mixed|null $event + * @param mixed $event * @return \Illuminate\Broadcasting\PendingBroadcast */ public function event($event = null) @@ -178,7 +181,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 +209,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(); } /** @@ -282,7 +294,11 @@ protected function resolve($name) throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported."); } - return $this->{$driverMethod}($config); + try { + return $this->{$driverMethod}($config); + } catch (Throwable $e) { + throw new RuntimeException("Failed to create broadcaster for connection \"{$name}\" with error: {$e->getMessage()}.", 0, $e); + } } /** @@ -475,6 +491,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/Broadcasters/RedisBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php index 9cb81c85af1d..15878575dec0 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php @@ -4,7 +4,11 @@ use Illuminate\Broadcasting\BroadcastException; use Illuminate\Contracts\Redis\Factory as Redis; +use Illuminate\Redis\Connections\PhpRedisClusterConnection; +use Illuminate\Redis\Connections\PredisClusterConnection; +use Illuminate\Redis\Connections\PredisConnection; use Illuminate\Support\Arr; +use Predis\Connection\Cluster\RedisCluster; use Predis\Connection\ConnectionException; use RedisException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -125,10 +129,30 @@ public function broadcast(array $channels, $event, array $payload = []) ]); try { - $connection->eval( - $this->broadcastMultipleChannelsScript(), - 0, $payload, ...$this->formatChannels($channels) - ); + if ($connection instanceof PhpRedisClusterConnection) { + foreach ($channels as $channel) { + $connection->publish($channel, $payload); + } + } elseif ($connection instanceof PredisClusterConnection && + $connection->client()->getConnection() instanceof RedisCluster) { + $randomClusterNodeConnection = new PredisConnection( + $connection->client()->getClientBy('slot', mt_rand(0, 16383)) + ); + + if ($events = $connection->getEventDispatcher()) { + $randomClusterNodeConnection->setEventDispatcher($events); + } + + $randomClusterNodeConnection->eval( + $this->broadcastMultipleChannelsScript(), + 0, $payload, ...$this->formatChannels($channels) + ); + } else { + $connection->eval( + $this->broadcastMultipleChannelsScript(), + 0, $payload, ...$this->formatChannels($channels) + ); + } } catch (ConnectionException|RedisException $e) { throw new BroadcastException( sprintf('Redis error: %s.', $e->getMessage()) 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/Batch.php b/src/Illuminate/Bus/Batch.php index 717d1c4ab11d..5d469cd12e95 100644 --- a/src/Illuminate/Bus/Batch.php +++ b/src/Illuminate/Bus/Batch.php @@ -168,12 +168,12 @@ public function add($jobs) if (is_array($job)) { $count += count($job); - return with($this->prepareBatchedChain($job), function ($chain) { - return $chain->first() - ->allOnQueue($this->options['queue'] ?? null) - ->allOnConnection($this->options['connection'] ?? null) - ->chain($chain->slice(1)->values()->all()); - }); + $chain = $this->prepareBatchedChain($job); + + return $chain->first() + ->allOnQueue($this->options['queue'] ?? null) + ->allOnConnection($this->options['connection'] ?? null) + ->chain($chain->slice(1)->values()->all()); } else { $job->withBatchId($this->id); @@ -242,11 +242,7 @@ public function recordSuccessfulJob(string $jobId) $counts = $this->decrementPendingJobs($jobId); if ($this->hasProgressCallbacks()) { - $batch = $this->fresh(); - - (new Collection($this->options['progress']))->each(function ($handler) use ($batch) { - $this->invokeHandlerCallback($handler, $batch); - }); + $this->invokeCallbacks('progress'); } if ($counts->pendingJobs === 0) { @@ -254,19 +250,11 @@ public function recordSuccessfulJob(string $jobId) } if ($counts->pendingJobs === 0 && $this->hasThenCallbacks()) { - $batch = $this->fresh(); - - (new Collection($this->options['then']))->each(function ($handler) use ($batch) { - $this->invokeHandlerCallback($handler, $batch); - }); + $this->invokeCallbacks('then'); } if ($counts->allJobsHaveRanExactlyOnce() && $this->hasFinallyCallbacks()) { - $batch = $this->fresh(); - - (new Collection($this->options['finally']))->each(function ($handler) use ($batch) { - $this->invokeHandlerCallback($handler, $batch); - }); + $this->invokeCallbacks('finally'); } } @@ -281,6 +269,18 @@ public function decrementPendingJobs(string $jobId) return $this->repository->decrementPendingJobs($this->id, $jobId); } + /** + * Invoke the callbacks of the given type. + */ + protected function invokeCallbacks(string $type, ?Throwable $e = null): void + { + $batch = $this->fresh(); + + foreach ($this->options[$type] ?? [] as $handler) { + $this->invokeHandlerCallback($handler, $batch, $e); + } + } + /** * Determine if the batch has finished executing. * @@ -346,28 +346,22 @@ public function recordFailedJob(string $jobId, $e) $this->cancel(); } - if ($this->hasProgressCallbacks() && $this->allowsFailures()) { - $batch = $this->fresh(); + if ($this->allowsFailures()) { + if ($this->hasProgressCallbacks()) { + $this->invokeCallbacks('progress', $e); + } - (new Collection($this->options['progress']))->each(function ($handler) use ($batch, $e) { - $this->invokeHandlerCallback($handler, $batch, $e); - }); + if ($this->hasFailureCallbacks()) { + $this->invokeCallbacks('failure', $e); + } } if ($counts->failedJobs === 1 && $this->hasCatchCallbacks()) { - $batch = $this->fresh(); - - (new Collection($this->options['catch']))->each(function ($handler) use ($batch, $e) { - $this->invokeHandlerCallback($handler, $batch, $e); - }); + $this->invokeCallbacks('catch', $e); } if ($counts->allJobsHaveRanExactlyOnce() && $this->hasFinallyCallbacks()) { - $batch = $this->fresh(); - - (new Collection($this->options['finally']))->each(function ($handler) use ($batch, $e) { - $this->invokeHandlerCallback($handler, $batch, $e); - }); + $this->invokeCallbacks('finally'); } } @@ -392,6 +386,14 @@ public function hasCatchCallbacks() return isset($this->options['catch']) && ! empty($this->options['catch']); } + /** + * Determine if the batch has "failure" callbacks. + */ + public function hasFailureCallbacks(): bool + { + return isset($this->options['failure']) && ! empty($this->options['failure']); + } + /** * Determine if the batch has "finally" callbacks. * 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..891573219c5a 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -60,9 +60,6 @@ class Dispatcher implements QueueingDispatcher /** * Create a new command dispatcher instance. - * - * @param \Illuminate\Contracts\Container\Container $container - * @param \Closure|null $queueResolver */ public function __construct(Container $container, ?Closure $queueResolver = null) { @@ -139,7 +136,6 @@ public function dispatchNow($command, $handler = null) /** * Attempt to find the batch with the given ID. * - * @param string $batchId * @return \Illuminate\Bus\Batch|null */ public function findBatch(string $batchId) @@ -150,7 +146,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 +157,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 +183,7 @@ public function hasCommandHandler($command) * Retrieve the handler for a command. * * @param mixed $command - * @return bool|mixed + * @return mixed */ public function getCommandHandler($command) { @@ -273,7 +269,6 @@ public function dispatchAfterResponse($command, $handler = null) /** * Set the pipes through which commands should be piped before dispatching. * - * @param array $pipes * @return $this */ public function pipeThrough(array $pipes) @@ -286,7 +281,6 @@ public function pipeThrough(array $pipes) /** * Map a command to a handler. * - * @param array $map * @return $this */ public function map(array $map) diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index 9538074d7be4..455322a49ff0 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -119,9 +119,7 @@ protected function ensureJobIsBatchable(object|array $job): void */ public function before($callback) { - $this->options['before'][] = $callback instanceof Closure - ? new SerializableClosure($callback) - : $callback; + $this->registerCallback('before', $callback); return $this; } @@ -144,9 +142,7 @@ public function beforeCallbacks() */ public function progress($callback) { - $this->options['progress'][] = $callback instanceof Closure - ? new SerializableClosure($callback) - : $callback; + $this->registerCallback('progress', $callback); return $this; } @@ -169,9 +165,7 @@ public function progressCallbacks() */ public function then($callback) { - $this->options['then'][] = $callback instanceof Closure - ? new SerializableClosure($callback) - : $callback; + $this->registerCallback('then', $callback); return $this; } @@ -194,9 +188,7 @@ public function thenCallbacks() */ public function catch($callback) { - $this->options['catch'][] = $callback instanceof Closure - ? new SerializableClosure($callback) - : $callback; + $this->registerCallback('catch', $callback); return $this; } @@ -219,9 +211,7 @@ public function catchCallbacks() */ public function finally($callback) { - $this->options['finally'][] = $callback instanceof Closure - ? new SerializableClosure($callback) - : $callback; + $this->registerCallback('finally', $callback); return $this; } @@ -237,14 +227,28 @@ public function finallyCallbacks() } /** - * Indicate that the batch should not be cancelled when a job within the batch fails. + * Indicate that the batch should not be canceled when a job within the batch fails. + * + * Optionally, add callbacks to be executed upon each job failure. + * + * @template TParam of (Closure(\Illuminate\Bus\Batch, \Throwable|null): mixed)|(callable(\Illuminate\Bus\Batch, \Throwable|null): mixed) * - * @param bool $allowFailures + * @param bool|TParam|array $param * @return $this */ - public function allowFailures($allowFailures = true) + public function allowFailures($param = true) { - $this->options['allowFailures'] = $allowFailures; + if (! is_bool($param)) { + $param = Arr::wrap($param); + + foreach ($param as $callback) { + if (is_callable($callback)) { + $this->registerCallback('failure', $callback); + } + } + } + + $this->options['allowFailures'] = ! ($param === false); return $this; } @@ -259,6 +263,26 @@ public function allowsFailures() return Arr::get($this->options, 'allowFailures', false) === true; } + /** + * Get the "failure" callbacks that have been registered with the pending batch. + * + * @return array + */ + public function failureCallbacks(): array + { + return $this->options['failure'] ?? []; + } + + /** + * Register a callback with proper serialization. + */ + private function registerCallback(string $type, Closure|callable $callback): void + { + $this->options[$type][] = $callback instanceof Closure + ? new SerializableClosure($callback) + : $callback; + } + /** * Set the name for the batch. * diff --git a/src/Illuminate/Bus/Queueable.php b/src/Illuminate/Bus/Queueable.php index c42614d9e65d..b9bbc0319d42 100644 --- a/src/Illuminate/Bus/Queueable.php +++ b/src/Illuminate/Bus/Queueable.php @@ -6,6 +6,7 @@ use Illuminate\Queue\CallQueuedClosure; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Laravel\SerializableClosure\SerializableClosure; use PHPUnit\Framework\Assert as PHPUnit; use RuntimeException; @@ -27,6 +28,20 @@ trait Queueable */ public $queue; + /** + * The job "group" the job should be sent to. + * + * @var string|null + */ + public $messageGroup; + + /** + * The job deduplicator callback the job should use to generate the deduplication ID. + * + * @var \Laravel\SerializableClosure\SerializableClosure|null + */ + public $deduplicator; + /** * The number of seconds before the job should be made available. * @@ -102,6 +117,38 @@ public function onQueue($queue) return $this; } + /** + * Set the desired job "group". + * + * This feature is only supported by some queues, such as Amazon SQS. + * + * @param \UnitEnum|string $group + * @return $this + */ + public function onGroup($group) + { + $this->messageGroup = enum_value($group); + + return $this; + } + + /** + * Set the desired job deduplicator callback. + * + * This feature is only supported by some queues, such as Amazon SQS FIFO. + * + * @param callable|null $deduplicator + * @return $this + */ + public function withDeduplicator($deduplicator) + { + $this->deduplicator = $deduplicator instanceof Closure + ? new SerializableClosure($deduplicator) + : $deduplicator; + + return $this; + } + /** * Set the desired connection for the chain. * @@ -204,11 +251,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; } @@ -221,9 +266,11 @@ public function chain($chain) */ public function prependToChain($job) { - $jobs = ChainedBatch::prepareNestedBatches(new Collection([$job])); + $jobs = ChainedBatch::prepareNestedBatches(Collection::wrap($job)); - $this->chained = Arr::prepend($this->chained, $this->serializeJob($jobs->first())); + foreach ($jobs->reverse() as $job) { + $this->chained = Arr::prepend($this->chained, $this->serializeJob($job)); + } return $this; } @@ -236,9 +283,11 @@ public function prependToChain($job) */ public function appendToChain($job) { - $jobs = ChainedBatch::prepareNestedBatches(new Collection([$job])); + $jobs = ChainedBatch::prepareNestedBatches(Collection::wrap($job)); - $this->chained = array_merge($this->chained, [$this->serializeJob($jobs->first())]); + foreach ($jobs as $job) { + $this->chained = array_merge($this->chained, [$this->serializeJob($job)]); + } return $this; } diff --git a/src/Illuminate/Cache/ApcStore.php b/src/Illuminate/Cache/ApcStore.php index 89c31a3f7f0c..99bf2ed38b0b 100755 --- a/src/Illuminate/Cache/ApcStore.php +++ b/src/Illuminate/Cache/ApcStore.php @@ -60,8 +60,8 @@ public function put($key, $value, $seconds) * Increment the value of an item in the cache. * * @param string $key - * @param mixed $value - * @return int|bool + * @param int $value + * @return int|false */ public function increment($key, $value = 1) { @@ -72,8 +72,8 @@ public function increment($key, $value = 1) * Decrement the value of an item in the cache. * * @param string $key - * @param mixed $value - * @return int|bool + * @param int $value + * @return int|false */ public function decrement($key, $value = 1) { diff --git a/src/Illuminate/Cache/ApcWrapper.php b/src/Illuminate/Cache/ApcWrapper.php index 43c6e328a8fe..b1e460ba40a1 100755 --- a/src/Illuminate/Cache/ApcWrapper.php +++ b/src/Illuminate/Cache/ApcWrapper.php @@ -23,7 +23,7 @@ public function get($key) * @param string $key * @param mixed $value * @param int $seconds - * @return array|bool + * @return bool */ public function put($key, $value, $seconds) { @@ -34,8 +34,8 @@ public function put($key, $value, $seconds) * Increment the value of an item in the cache. * * @param string $key - * @param mixed $value - * @return int|bool + * @param int $value + * @return int|false */ public function increment($key, $value) { @@ -46,8 +46,8 @@ public function increment($key, $value) * Decrement the value of an item in the cache. * * @param string $key - * @param mixed $value - * @return int|bool + * @param int $value + * @return int|false */ public function decrement($key, $value) { diff --git a/src/Illuminate/Cache/ArrayLock.php b/src/Illuminate/Cache/ArrayLock.php index 2eb5054dd544..1c438bcc3379 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() { @@ -94,7 +94,7 @@ protected function getCurrentOwner() } /** - * Releases this lock in disregard of ownership. + * Releases this lock regardless of ownership. * * @return void */ diff --git a/src/Illuminate/Cache/ArrayStore.php b/src/Illuminate/Cache/ArrayStore.php index 112501831822..a9abb569cffe 100644 --- a/src/Illuminate/Cache/ArrayStore.php +++ b/src/Illuminate/Cache/ArrayStore.php @@ -13,14 +13,14 @@ class ArrayStore extends TaggableStore implements LockProvider /** * The array of stored values. * - * @var array + * @var array */ protected $storage = []; /** * The array of locks. * - * @var array + * @var array */ public $locks = []; @@ -41,6 +41,30 @@ public function __construct($serializesValues = false) $this->serializesValues = $serializesValues; } + /** + * Get all of the cached values and their expiration times. + * + * @param bool $unserialize + * @return array + */ + public function all($unserialize = true) + { + if ($unserialize === false || $this->serializesValues === false) { + return $this->storage; + } + + $storage = []; + + foreach ($this->storage as $key => $data) { + $storage[$key] = [ + 'value' => unserialize($data['value']), + 'expiresAt' => $data['expiresAt'], + ]; + } + + return $storage; + } + /** * Retrieve an item from the cache by key. * diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 0a0c2de5e171..2e793f6083d2 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -169,6 +169,82 @@ protected function createArrayDriver(array $config) return $this->repository(new ArrayStore($config['serialize'] ?? false), $config); } + /** + * Create an instance of the database cache driver. + * + * @param array $config + * @return \Illuminate\Cache\Repository + */ + protected function createDatabaseDriver(array $config) + { + $connection = $this->app['db']->connection($config['connection'] ?? null); + + $store = new DatabaseStore( + $connection, + $config['table'], + $this->getPrefix($config), + $config['lock_table'] ?? 'cache_locks', + $config['lock_lottery'] ?? [2, 100], + $config['lock_timeout'] ?? 86400, + ); + + return $this->repository( + $store->setLockConnection( + $this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null) + ), + $config + ); + } + + /** + * Create an instance of the DynamoDB cache driver. + * + * @param array $config + * @return \Illuminate\Cache\Repository + */ + protected function createDynamodbDriver(array $config) + { + $client = $this->newDynamodbClient($config); + + return $this->repository( + new DynamoDbStore( + $client, + $config['table'], + $config['attributes']['key'] ?? 'key', + $config['attributes']['value'] ?? 'value', + $config['attributes']['expiration'] ?? 'expires_at', + $this->getPrefix($config) + ), + $config + ); + } + + /** + * Create new DynamoDb Client instance. + * + * @return \Aws\DynamoDb\DynamoDbClient + */ + protected function newDynamodbClient(array $config) + { + $dynamoConfig = [ + 'region' => $config['region'], + 'version' => 'latest', + 'endpoint' => $config['endpoint'] ?? null, + ]; + + if (! empty($config['key']) && ! empty($config['secret'])) { + $dynamoConfig['credentials'] = Arr::only( + $config, ['key', 'secret'] + ); + + if (! empty($config['token'])) { + $dynamoConfig['credentials']['token'] = $config['token']; + } + } + + return new DynamoDbClient($dynamoConfig); + } + /** * Create an instance of the file cache driver. * @@ -235,79 +311,38 @@ protected function createRedisDriver(array $config) } /** - * Create an instance of the database cache driver. + * Create an instance of the session cache driver. * * @param array $config * @return \Illuminate\Cache\Repository */ - protected function createDatabaseDriver(array $config) + protected function createSessionDriver(array $config) { - $connection = $this->app['db']->connection($config['connection'] ?? null); - - $store = new DatabaseStore( - $connection, - $config['table'], - $this->getPrefix($config), - $config['lock_table'] ?? 'cache_locks', - $config['lock_lottery'] ?? [2, 100], - $config['lock_timeout'] ?? 86400, - ); - return $this->repository( - $store->setLockConnection( - $this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null) + new SessionStore( + $this->getSession(), + $config['key'] ?? '_cache', ), $config ); } /** - * Create an instance of the DynamoDB cache driver. + * Get the session store implementation. * - * @param array $config - * @return \Illuminate\Cache\Repository - */ - protected function createDynamodbDriver(array $config) - { - $client = $this->newDynamodbClient($config); - - return $this->repository( - new DynamoDbStore( - $client, - $config['table'], - $config['attributes']['key'] ?? 'key', - $config['attributes']['value'] ?? 'value', - $config['attributes']['expiration'] ?? 'expires_at', - $this->getPrefix($config) - ), - $config - ); - } - - /** - * Create new DynamoDb Client instance. + * @return \Illuminate\Contracts\Session\Session * - * @return \Aws\DynamoDb\DynamoDbClient + * @throws \InvalidArgumentException */ - protected function newDynamodbClient(array $config) + protected function getSession() { - $dynamoConfig = [ - 'region' => $config['region'], - 'version' => 'latest', - 'endpoint' => $config['endpoint'] ?? null, - ]; - - if (! empty($config['key']) && ! empty($config['secret'])) { - $dynamoConfig['credentials'] = Arr::only( - $config, ['key', 'secret'] - ); + $session = $this->app['session'] ?? null; - if (! empty($config['token'])) { - $dynamoConfig['credentials']['token'] = $config['token']; - } + if (! $session) { + throw new InvalidArgumentException('Session store requires session manager to be available in container.'); } - return new DynamoDbClient($dynamoConfig); + return $session; } /** 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 d899ef09d609..6c24e33346ce 100644 --- a/src/Illuminate/Cache/MemoizedStore.php +++ b/src/Illuminate/Cache/MemoizedStore.php @@ -2,9 +2,11 @@ namespace Illuminate\Cache; +use BadMethodCallException; +use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Store; -class MemoizedStore implements Store +class MemoizedStore implements LockProvider, Store { /** * The memoized cache values. @@ -106,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 */ @@ -160,6 +163,39 @@ public function forever($key, $value) return $this->repository->forever($key, $value); } + /** + * Get a lock instance. + * + * @param string $name + * @param int $seconds + * @param string|null $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function lock($name, $seconds = 0, $owner = null) + { + if (! $this->repository->getStore() instanceof LockProvider) { + throw new BadMethodCallException('This cache store does not support locks.'); + } + + return $this->repository->getStore()->lock(...func_get_args()); + } + + /** + * Restore a lock instance using the owner identifier. + * + * @param string $name + * @param string $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function restoreLock($name, $owner) + { + if (! $this->repository instanceof LockProvider) { + throw new BadMethodCallException('This cache store does not support locks.'); + } + + return $this->repository->resoreLock(...func_get_args()); + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index 67588ffbb1e1..f28c57a4d240 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -210,7 +210,7 @@ public function attempts($key) * Reset the number of attempts for the given key. * * @param string $key - * @return mixed + * @return bool */ public function resetAttempts($key) { diff --git a/src/Illuminate/Cache/RateLimiting/Limit.php b/src/Illuminate/Cache/RateLimiting/Limit.php index 1a14009640e8..351bbf11fb8f 100644 --- a/src/Illuminate/Cache/RateLimiting/Limit.php +++ b/src/Illuminate/Cache/RateLimiting/Limit.php @@ -25,6 +25,13 @@ class Limit */ public $decaySeconds; + /** + * The after callback used to determine if the limiter should be hit. + * + * @var ?callable + */ + public $afterCallback = null; + /** * The response generator callback. * @@ -129,6 +136,19 @@ public function by($key) return $this; } + /** + * Set the callback to determine if the limiter should be hit. + * + * @param callable $callback + * @return $this + */ + public function after($callback) + { + $this->afterCallback = $callback; + + return $this; + } + /** * Set the callback that should generate the response when the limit is exceeded. * diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 33cdf87307c7..4f18dcf5d1b9 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(); @@ -286,7 +286,7 @@ public function flushStaleTags() /** * Begin executing a new tags operation. * - * @param array|mixed $names + * @param mixed $names * @return \Illuminate\Cache\RedisTaggedCache */ public function tags($names) @@ -462,7 +462,7 @@ protected function serialize($value) */ protected function shouldBeStoredWithoutSerialization($value): bool { - return is_numeric($value) && ! in_array($value, [INF, -INF]) && ! is_nan($value); + return is_numeric($value) && is_finite($value); } /** diff --git a/src/Illuminate/Cache/RedisTagSet.php b/src/Illuminate/Cache/RedisTagSet.php index 267c11607cd4..82369ff4ed00 100644 --- a/src/Illuminate/Cache/RedisTagSet.php +++ b/src/Illuminate/Cache/RedisTagSet.php @@ -13,7 +13,7 @@ class RedisTagSet extends TagSet * * @param string $key * @param int|null $ttl - * @param string $updateWhen + * @param string|null $updateWhen * @return void */ public function addEntry(string $key, ?int $ttl = null, $updateWhen = null) @@ -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/RedisTaggedCache.php b/src/Illuminate/Cache/RedisTaggedCache.php index 69053266d33d..1728a969c0c2 100644 --- a/src/Illuminate/Cache/RedisTaggedCache.php +++ b/src/Illuminate/Cache/RedisTaggedCache.php @@ -4,6 +4,10 @@ use Illuminate\Cache\Events\CacheFlushed; use Illuminate\Cache\Events\CacheFlushing; +use Illuminate\Redis\Connections\PhpRedisClusterConnection; +use Illuminate\Redis\Connections\PhpRedisConnection; +use Illuminate\Redis\Connections\PredisClusterConnection; +use Illuminate\Redis\Connections\PredisConnection; class RedisTaggedCache extends TaggedCache { @@ -107,6 +111,65 @@ public function forever($key, $value) * @return bool */ public function flush() + { + $connection = $this->store->connection(); + + if ($connection instanceof PredisClusterConnection || + $connection instanceof PhpRedisClusterConnection) { + return $this->flushClusteredConnection(); + } + + $this->event(new CacheFlushing($this->getName())); + + $redisPrefix = match (true) { + $connection instanceof PhpRedisConnection => $connection->client()->getOption(\Redis::OPT_PREFIX), + $connection instanceof PredisConnection => $connection->client()->getOptions()->prefix, + }; + + $cachePrefix = $redisPrefix.$this->store->getPrefix(); + + $cacheTags = []; + $keysToBeDeleted = []; + + foreach ($this->tags->getNames() as $name) { + $cacheTags[] = $cachePrefix.$this->tags->tagId($name); + } + + foreach ($this->tags->entries() as $entry) { + $keysToBeDeleted[] = $this->store->getPrefix().$entry; + } + + $script = <<<'LUA' + local prefix = table.remove(ARGV, 1) + + for i, key in ipairs(KEYS) do + redis.call('DEL', key) + + for j, arg in ipairs(ARGV) do + local zkey = string.gsub(key, prefix, "") + redis.call('ZREM', arg, zkey) + end + end + LUA; + + $connection->eval( + $script, + count($keysToBeDeleted), + ...$keysToBeDeleted, + ...[$cachePrefix, ...$cacheTags] + ); + + $this->event(new CacheFlushed($this->getName())); + + return true; + } + + /** + * Remove all items from the cache. + * + * @return bool + */ + protected function flushClusteredConnection() { $this->event(new CacheFlushing($this->getName())); diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 3eb6f700ed01..880ed23f776c 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; } @@ -595,7 +596,7 @@ public function clear(): bool /** * Begin executing a new tags operation if the store supports it. * - * @param array|mixed $names + * @param mixed $names * @return \Illuminate\Cache\TaggedCache * * @throws \BadMethodCallException diff --git a/src/Illuminate/Cache/SessionStore.php b/src/Illuminate/Cache/SessionStore.php new file mode 100644 index 000000000000..0ed5a4da0609 --- /dev/null +++ b/src/Illuminate/Cache/SessionStore.php @@ -0,0 +1,206 @@ +key = $key; + $this->session = $session; + } + + /** + * Get all of the cached values and their expiration times. + * + * @return array + */ + public function all() + { + return $this->session->get($this->key, []); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + if (! $this->session->exists($this->itemKey($key))) { + return; + } + + $item = $this->session->get($this->itemKey($key)); + + $expiresAt = $item['expiresAt'] ?? 0; + + if ($this->isExpired($expiresAt)) { + $this->forget($key); + + return; + } + + return $item['value']; + } + + /** + * Determine if the given expiration time is expired. + * + * @param int|float $expiresAt + * @return bool + */ + protected function isExpired($expiresAt) + { + return $expiresAt !== 0 && (Carbon::now()->getPreciseTimestamp(3) / 1000) >= $expiresAt; + } + + /** + * Store an item in the cache for a given number of seconds. + * + * @param string $key + * @param mixed $value + * @param int $seconds + * @return bool + */ + public function put($key, $value, $seconds) + { + $this->session->put($this->itemKey($key), [ + 'value' => $value, + 'expiresAt' => $this->toTimestamp($seconds), + ]); + + return true; + } + + /** + * Get the UNIX timestamp, with milliseconds, for the given number of seconds in the future. + * + * @param int $seconds + * @return float + */ + protected function toTimestamp($seconds) + { + return $seconds > 0 ? (Carbon::now()->getPreciseTimestamp(3) / 1000) + $seconds : 0; + } + + /** + * Increment the value of an item in the cache. + * + * @param string $key + * @param mixed $value + * @return int + */ + public function increment($key, $value = 1) + { + if (! is_null($existing = $this->get($key))) { + return tap(((int) $existing) + $value, function ($incremented) use ($key) { + $this->session->put($this->itemKey("{$key}.value"), $incremented); + }); + } + + $this->forever($key, $value); + + return $value; + } + + /** + * Decrement the value of an item in the cache. + * + * @param string $key + * @param mixed $value + * @return int + */ + public function decrement($key, $value = 1) + { + return $this->increment($key, $value * -1); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return bool + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + if ($this->session->exists($this->itemKey($key))) { + $this->session->forget($this->itemKey($key)); + + return true; + } + + return false; + } + + /** + * Remove all items from the cache. + * + * @return bool + */ + public function flush() + { + $this->session->put($this->key, []); + + return true; + } + + /** + * Get the cache key prefix. + * + * @return string + */ + public function itemKey($key) + { + return "{$this->key}.{$key}"; + } + + /** + * Get the cache key prefix. + * + * @return string + */ + public function getPrefix() + { + return ''; + } +} diff --git a/src/Illuminate/Cache/TaggableStore.php b/src/Illuminate/Cache/TaggableStore.php index 6a12b45dbfd3..41eca631d80f 100644 --- a/src/Illuminate/Cache/TaggableStore.php +++ b/src/Illuminate/Cache/TaggableStore.php @@ -9,7 +9,7 @@ abstract class TaggableStore implements Store /** * Begin executing a new tags operation. * - * @param array|mixed $names + * @param mixed $names * @return \Illuminate\Cache\TaggedCache */ public function tags($names) diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index d9b7561db2cf..e6586edd7f95 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -4,9 +4,15 @@ use ArgumentCountError; use ArrayAccess; +use Closure; +use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; +use JsonSerializable; use Random\Randomizer; +use Traversable; +use WeakMap; class Arr { @@ -23,6 +29,21 @@ public static function accessible($value) return is_array($value) || $value instanceof ArrayAccess; } + /** + * Determine whether the given value is arrayable. + * + * @param mixed $value + * @return bool + */ + public static function arrayable($value) + { + return is_array($value) + || $value instanceof Arrayable + || $value instanceof Traversable + || $value instanceof Jsonable + || $value instanceof JsonSerializable; + } + /** * Add an element to an array using "dot" notation if it doesn't exist. * @@ -42,8 +63,12 @@ public static function add($array, $key, $value) /** * Get an array item from an array using "dot" notation. + * + * @return array + * + * @throws \InvalidArgumentException */ - public static function array(ArrayAccess|array $array, string|int|null $key, ?array $default = null): array + public static function array(ArrayAccess|array $array, string|int|null $key, ?array $default = null) { $value = Arr::get($array, $key, $default); @@ -58,6 +83,8 @@ public static function array(ArrayAccess|array $array, string|int|null $key, ?ar /** * Get a boolean item from an array using "dot" notation. + * + * @throws \InvalidArgumentException */ public static function boolean(ArrayAccess|array $array, string|int|null $key, ?bool $default = null): bool { @@ -84,12 +111,10 @@ public static function collapse($array) foreach ($array as $values) { if ($values instanceof Collection) { - $values = $values->all(); - } elseif (! is_array($values)) { - continue; + $results[] = $values->all(); + } elseif (is_array($values)) { + $results[] = $values; } - - $results[] = $values; } return array_merge([], ...$results); @@ -209,7 +234,7 @@ public static function exists($array, $key) return $array->offsetExists($key); } - if (is_float($key)) { + if (is_float($key) || is_null($key)) { $key = (string) $key; } @@ -235,6 +260,10 @@ public static function first($array, ?callable $callback = null, $default = null return value($default); } + if (is_array($array)) { + return array_first($array); + } + foreach ($array as $item) { return $item; } @@ -242,13 +271,9 @@ public static function first($array, ?callable $callback = null, $default = null return value($default); } - foreach ($array as $key => $value) { - if ($callback($value, $key)) { - return $value; - } - } + $key = array_find_key($array, $callback); - return value($default); + return $key !== null ? $array[$key] : value($default); } /** @@ -266,7 +291,7 @@ public static function first($array, ?callable $callback = null, $default = null public static function last($array, ?callable $callback = null, $default = null) { if (is_null($callback)) { - return empty($array) ? value($default) : end($array); + return empty($array) ? value($default) : array_last($array); } return static::first(array_reverse($array, true), $callback, $default); @@ -320,6 +345,8 @@ public static function flatten($array, $depth = INF) /** * Get a float item from an array using "dot" notation. + * + * @throws \InvalidArgumentException */ public static function float(ArrayAccess|array $array, string|int|null $key, ?float $default = null): float { @@ -378,6 +405,32 @@ public static function forget(&$array, $keys) } } + /** + * Get the underlying array of items from the given argument. + * + * @template TKey of array-key = array-key + * @template TValue = mixed + * + * @param array|Enumerable|Arrayable|WeakMap|Traversable|Jsonable|JsonSerializable|object $items + * @return ($items is WeakMap ? list : array) + * + * @throws \InvalidArgumentException + */ + public static function from($items) + { + return match (true) { + is_array($items) => $items, + $items instanceof Enumerable => $items->all(), + $items instanceof Arrayable => $items->toArray(), + $items instanceof WeakMap => iterator_to_array($items, false), + $items instanceof Traversable => iterator_to_array($items), + $items instanceof Jsonable => json_decode($items->toJson(), true), + $items instanceof JsonSerializable => (array) $items->jsonSerialize(), + is_object($items) => (array) $items, + default => throw new InvalidArgumentException('Items cannot be represented by a scalar value.'), + }; + } + /** * Get an item from an array using "dot" notation. * @@ -401,7 +454,7 @@ public static function get($array, $key, $default = null) } if (! str_contains($key, '.')) { - return $array[$key] ?? value($default); + return value($default); } foreach (explode('.', $key) as $segment) { @@ -449,6 +502,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. * @@ -481,14 +558,40 @@ 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) + { + return array_all($array, $callback); + } + + /** + * 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) + { + return array_any($array, $callback); + } + /** * Get an integer item from an array using "dot" notation. + * + * @throws \InvalidArgumentException */ public static function integer(ArrayAccess|array $array, string|int|null $key, ?int $default = null): int { $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)) ); @@ -542,7 +645,7 @@ public static function join($array, $glue, $finalGlue = '') } if (count($array) === 1) { - return end($array); + return array_last($array); } $finalItem = array_pop($array); @@ -553,7 +656,7 @@ public static function join($array, $glue, $finalGlue = '') /** * Key an associative array by a field or using a callback. * - * @param array $array + * @param iterable $array * @param callable|array|string $keyBy * @return array */ @@ -616,8 +719,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) @@ -627,7 +730,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 @@ -635,7 +740,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; @@ -651,15 +758,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]; } @@ -866,6 +973,23 @@ public static function set(&$array, $key, $value) return $array; } + /** + * Push an item into an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string|int|null $key + * @param mixed $values + * @return array + */ + public static function push(ArrayAccess|array &$array, string|int|null $key, mixed ...$values): array + { + $target = static::array($array, $key, []); + + array_push($target, ...$values); + + return static::set($array, $key, $target); + } + /** * Shuffle the given array and return the result. * @@ -878,10 +1002,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 @@ -908,7 +1032,7 @@ public static function sole($array, ?callable $callback = null) /** * Sort the array using the given callback or "dot" notation. * - * @param array $array + * @param iterable $array * @param callable|array|string|null $callback * @return array */ @@ -920,7 +1044,7 @@ public static function sort($array, $callback = null) /** * Sort the array in descending order using the given callback or "dot" notation. * - * @param array $array + * @param iterable $array * @param callable|array|string|null $callback * @return array */ @@ -972,6 +1096,8 @@ public static function sortRecursiveDesc($array, $options = SORT_REGULAR) /** * Get a string item from an array using "dot" notation. + * + * @throws \InvalidArgumentException */ public static function string(ArrayAccess|array $array, string|int|null $key, ?string $default = null): string { @@ -989,7 +1115,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) @@ -1012,7 +1138,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 23e1af7bbfee..08a852fe19fa 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -95,7 +95,7 @@ public function median($key = null) return; } - $middle = (int) ($count / 2); + $middle = intdiv($count, 2); if ($count % 2) { return $values->get($middle); @@ -165,6 +165,10 @@ public function collapseWithKeys() $results[$key] = $values; } + if (! $results) { + return new static; + } + return new static(array_replace(...$results)); } @@ -180,9 +184,7 @@ public function contains($key, $operator = null, $value = null) { if (func_num_args() === 1) { if ($this->useAsCallable($key)) { - $placeholder = new stdClass; - - return $this->first($key, $placeholder) !== $placeholder; + return array_any($this->items, $key); } return in_array($key, $this->items); @@ -224,6 +226,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. * @@ -441,8 +456,7 @@ public function flip() /** * Remove an item from the collection by key. * - * \Illuminate\Contracts\Support\Arrayable|iterable|TKey $keys - * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|TKey $keys * @return $this */ public function forget($keys) @@ -459,12 +473,14 @@ public function forget($keys) * * @template TGetDefault * - * @param TKey $key + * @param TKey|null $key * @param TGetDefault|(\Closure(): TGetDefault) $default * @return TValue|TGetDefault */ public function get($key, $default = null) { + $key ??= ''; + if (array_key_exists($key, $this->items)) { return $this->items[$key]; } @@ -483,8 +499,8 @@ public function get($key, $default = null) */ public function getOrPut($key, $value) { - if (array_key_exists($key, $this->items)) { - return $this->items[$key]; + if (array_key_exists($key ?? '', $this->items)) { + return $this->items[$key ?? '']; } $this->offsetSet($key, $value = value($value)); @@ -523,8 +539,9 @@ public function groupBy($groupBy, $preserveKeys = false) foreach ($groupKeys as $groupKey) { $groupKey = match (true) { is_bool($groupKey) => (int) $groupKey, - $groupKey instanceof \BackedEnum => $groupKey->value, + $groupKey instanceof \UnitEnum => enum_value($groupKey), $groupKey instanceof \Stringable => (string) $groupKey, + is_null($groupKey) => (string) $groupKey, default => $groupKey, }; @@ -562,6 +579,10 @@ public function keyBy($keyBy) foreach ($this->items as $key => $item) { $resolvedKey = $keyBy($item, $key); + if ($resolvedKey instanceof \UnitEnum) { + $resolvedKey = enum_value($resolvedKey); + } + if (is_object($resolvedKey)) { $resolvedKey = (string) $resolvedKey; } @@ -582,13 +603,7 @@ public function has($key) { $keys = is_array($key) ? $key : func_get_args(); - foreach ($keys as $value) { - if (! array_key_exists($value, $this->items)) { - return false; - } - } - - return true; + return array_all($keys, fn ($key) => array_key_exists($key ?? '', $this->items)); } /** @@ -605,13 +620,7 @@ public function hasAny($key) $keys = is_array($key) ? $key : func_get_args(); - foreach ($keys as $value) { - if (array_key_exists($value, $this->items)) { - return true; - } - } - - return false; + return array_any($keys, fn ($key) => array_key_exists($key ?? '', $this->items)); } /** @@ -712,12 +721,17 @@ public function isEmpty() } /** - * Determine if the collection contains a single item. + * Determine if the collection contains exactly one item. If a callback is provided, determine if exactly one item matches the condition. * + * @param (callable(TValue, TKey): bool)|null $callback * @return bool */ - public function containsOneItem() + public function containsOneItem(?callable $callback = null): bool { + if ($callback) { + return $this->filter($callback)->count() === 1; + } + return $this->count() === 1; } @@ -726,7 +740,7 @@ public function containsOneItem() * * @param string $glue * @param string $finalGlue - * @return string + * @return TValue|string */ public function join($glue, $finalGlue = '') { @@ -778,8 +792,8 @@ public function last(?callable $callback = null, $default = null) /** * Get the values of a given key. * - * @param string|int|array|null $value - * @param string|null $key + * @param \Closure|string|int|array|null $value + * @param \Closure|string|null $key * @return static */ public function pluck($value, $key = null) @@ -983,7 +997,7 @@ public function select($keys) * Get and remove the last N items from the collection. * * @param int $count - * @return static|TValue|null + * @return ($count is 1 ? TValue|null : static) */ public function pop($count = 1) { @@ -1105,7 +1119,7 @@ public function put($key, $value) * * @param (callable(self): int)|int|null $number * @param bool $preserveKeys - * @return static|TValue + * @return ($number is null ? TValue : static) * * @throws \InvalidArgumentException */ @@ -1167,13 +1181,7 @@ public function search($value, $strict = false) return array_search($value, $this->items, $strict); } - foreach ($this->items as $key => $item) { - if ($value($item, $key)) { - return $key; - } - } - - return false; + return array_find_key($this->items, $value) ?? false; } /** @@ -1227,8 +1235,8 @@ public function after($value, $strict = false) /** * Get and remove the first N items from the collection. * - * @param int $count - * @return static|TValue|null + * @param int<0, max> $count + * @return ($count is 1 ? TValue|null : static) * * @throws \InvalidArgumentException */ @@ -1381,7 +1389,7 @@ public function splitIn($numberOfGroups) /** * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. * - * @param (callable(TValue, TKey): bool)|string $key + * @param (callable(TValue, TKey): bool)|string|null $key * @param mixed $operator * @param mixed $value * @return TValue @@ -1720,8 +1728,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) { @@ -1833,7 +1845,7 @@ public function getIterator(): Traversable /** * Count the number of items in the collection. * - * @return int + * @return int<0, max> */ public function count(): int { @@ -1843,7 +1855,7 @@ public function count(): int /** * Count the number of items in the collection by a field or using a callback. * - * @param (callable(TValue, TKey): array-key)|string|null $countBy + * @param (callable(TValue, TKey): array-key|\UnitEnum)|string|null $countBy * @return static */ public function countBy($countBy = null) diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 78187b785697..b59af1892dc6 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -985,7 +985,7 @@ public function sole($key = null, $operator = null, $value = null); /** * Get the first item in the collection but throw an exception if no matching items exist. * - * @param (callable(TValue, TKey): bool)|string $key + * @param (callable(TValue, TKey): bool)|string|null $key * @param mixed $operator * @param mixed $value * @return TValue @@ -1269,6 +1269,15 @@ public function jsonSerialize(): mixed; */ public function toJson($options = 0); + /** + * Get the collection of items as pretty print formatted JSON. + * + * + * @param int $options + * @return string + */ + public function toPrettyJson(int $options = 0); + /** * Get a CachingIterator instance. * 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..95b61720afc4 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -4,6 +4,8 @@ use ArrayIterator; use Closure; +use DateInterval; +use DateTimeImmutable; use DateTimeInterface; use Generator; use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString; @@ -287,6 +289,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. * @@ -304,7 +319,7 @@ public function crossJoin(...$arrays) /** * Count the number of items in the collection by a field or using a callback. * - * @param (callable(TValue, TKey): array-key)|string|null $countBy + * @param (callable(TValue, TKey): array-key|\UnitEnum)|string|null $countBy * @return static */ public function countBy($countBy = null) @@ -317,7 +332,7 @@ public function countBy($countBy = null) $counts = []; foreach ($this as $key => $value) { - $group = $countBy($value, $key); + $group = enum_value($countBy($value, $key)); if (empty($counts[$group])) { $counts[$group] = 0; @@ -777,12 +792,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; @@ -1325,7 +1344,7 @@ public function split($numberOfGroups) /** * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. * - * @param (callable(TValue, TKey): bool)|string $key + * @param (callable(TValue, TKey): bool)|string|null $key * @param mixed $operator * @param mixed $value * @return TValue @@ -1350,7 +1369,7 @@ public function sole($key = null, $operator = null, $value = null) /** * Get the first item in the collection but throw an exception if no matching items exist. * - * @param (callable(TValue, TKey): bool)|string $key + * @param (callable(TValue, TKey): bool)|string|null $key * @param mixed $operator * @param mixed $value * @return TValue @@ -1611,17 +1630,22 @@ public function takeUntil($value) } /** - * Take items in the collection until a given point in time. + * Take items in the collection until a given point in time, with an optional callback on timeout. * * @param \DateTimeInterface $timeout + * @param callable(TValue|null, TKey|null): mixed|null $callback * @return static */ - public function takeUntilTimeout(DateTimeInterface $timeout) + public function takeUntilTimeout(DateTimeInterface $timeout, ?callable $callback = null) { $timeout = $timeout->getTimestamp(); - return new static(function () use ($timeout) { + return new static(function () use ($timeout, $callback) { if ($this->now() >= $timeout) { + if ($callback) { + $callback(null, null); + } + return; } @@ -1629,6 +1653,10 @@ public function takeUntilTimeout(DateTimeInterface $timeout) yield $key => $value; if ($this->now() >= $timeout) { + if ($callback) { + $callback($value, $key); + } + break; } } @@ -1746,6 +1774,42 @@ public function values() }); } + /** + * Run the given callback every time the interval has passed. + * + * @return static + */ + public function withHeartbeat(DateInterval|int $interval, callable $callback) + { + $seconds = is_int($interval) ? $interval : $this->intervalSeconds($interval); + + return new static(function () use ($seconds, $callback) { + $start = $this->now(); + + foreach ($this as $key => $value) { + $now = $this->now(); + + if (($now - $start) >= $seconds) { + $callback(); + + $start = $now; + } + + yield $key => $value; + } + }); + } + + /** + * Get the total seconds from the given interval. + */ + protected function intervalSeconds(DateInterval $interval): int + { + $start = new DateTimeImmutable(); + + return $start->add($interval)->getTimestamp() - $start->getTimestamp(); + } + /** * Zip the collection together with one or more arrays. * @@ -1762,9 +1826,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 +1933,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 d2894529ed6e..9b6c4cf6249a 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -12,12 +12,9 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\HigherOrderCollectionProxy; -use InvalidArgumentException; use JsonSerializable; -use Traversable; use UnexpectedValueException; use UnitEnum; -use WeakMap; use function Illuminate\Support\enum_value; @@ -511,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) @@ -987,6 +984,17 @@ public function toJson($options = 0) return json_encode($this->jsonSerialize(), $options); } + /** + * Get the collection of items as pretty print formatted JSON. + * + * @param int $options + * @return string + */ + public function toPrettyJson(int $options = 0) + { + return $this->toJson(JSON_PRETTY_PRINT | $options); + } + /** * Get a CachingIterator instance. * @@ -1059,17 +1067,9 @@ public function __get($key) */ protected function getArrayableItems($items) { - return match (true) { - is_array($items) => $items, - $items instanceof WeakMap => throw new InvalidArgumentException('Collections can not be created using instances of WeakMap.'), - $items instanceof Enumerable => $items->all(), - $items instanceof Arrayable => $items->toArray(), - $items instanceof Traversable => iterator_to_array($items), - $items instanceof Jsonable => json_decode($items->toJson(), true), - $items instanceof JsonSerializable => (array) $items->jsonSerialize(), - $items instanceof UnitEnum => [$items], - default => (array) $items, - }; + return is_null($items) || is_scalar($items) || $items instanceof UnitEnum + ? Arr::wrap($items) + : Arr::from($items); } /** diff --git a/src/Illuminate/Collections/Traits/TransformsToResourceCollection.php b/src/Illuminate/Collections/Traits/TransformsToResourceCollection.php index 22143b356c48..86aaebd4b11d 100644 --- a/src/Illuminate/Collections/Traits/TransformsToResourceCollection.php +++ b/src/Illuminate/Collections/Traits/TransformsToResourceCollection.php @@ -2,9 +2,12 @@ namespace Illuminate\Support\Traits; +use Illuminate\Database\Eloquent\Attributes\UseResource; +use Illuminate\Database\Eloquent\Attributes\UseResourceCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Resources\Json\ResourceCollection; use LogicException; +use ReflectionClass; trait TransformsToResourceCollection { @@ -47,6 +50,18 @@ protected function guessResourceCollection(): ResourceCollection throw_unless(method_exists($className, 'guessResourceName'), LogicException::class, sprintf('Expected class %s to implement guessResourceName method. Make sure the model uses the TransformsToResource trait.', $className)); + $useResourceCollection = $this->resolveResourceCollectionFromAttribute($className); + + if ($useResourceCollection !== null && class_exists($useResourceCollection)) { + return new $useResourceCollection($this); + } + + $useResource = $this->resolveResourceFromAttribute($className); + + if ($useResource !== null && class_exists($useResource)) { + return $useResource::collection($this); + } + $resourceClasses = $className::guessResourceName(); foreach ($resourceClasses as $resourceClass) { @@ -65,4 +80,42 @@ protected function guessResourceCollection(): ResourceCollection throw new LogicException(sprintf('Failed to find resource class for model [%s].', $className)); } + + /** + * Get the resource class from the class attribute. + * + * @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $class + * @return class-string<*>|null + */ + protected function resolveResourceFromAttribute(string $class): ?string + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new ReflectionClass($class))->getAttributes(UseResource::class); + + return $attributes !== [] + ? $attributes[0]->newInstance()->class + : null; + } + + /** + * Get the resource collection class from the class attribute. + * + * @param class-string<\Illuminate\Http\Resources\Json\ResourceCollection> $class + * @return class-string<*>|null + */ + protected function resolveResourceCollectionFromAttribute(string $class): ?string + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new ReflectionClass($class))->getAttributes(UseResourceCollection::class); + + return $attributes !== [] + ? $attributes[0]->newInstance()->class + : null; + } } diff --git a/src/Illuminate/Collections/composer.json b/src/Illuminate/Collections/composer.json index 8d9c96125a47..107e36a5afb0 100644 --- a/src/Illuminate/Collections/composer.json +++ b/src/Illuminate/Collections/composer.json @@ -17,7 +17,9 @@ "php": "^8.2", "illuminate/conditionable": "^12.0", "illuminate/contracts": "^12.0", - "illuminate/macroable": "^12.0" + "illuminate/macroable": "^12.0", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 55844559e711..ed94004f47fc 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -13,7 +13,7 @@ * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $value * @return \Illuminate\Support\Collection */ - function collect($value = []) + function collect($value = []): Collection { return new Collection($value); } @@ -77,9 +77,9 @@ function data_get($target, $key, $default = null) $segment = match ($segment) { '\*' => '*', '\{first}' => '{first}', - '{first}' => array_key_first(is_array($target) ? $target : (new Collection($target))->all()), + '{first}' => array_key_first(Arr::from($target)), '\{last}' => '{last}', - '{last}' => array_key_last(is_array($target) ? $target : (new Collection($target))->all()), + '{last}' => array_key_last(Arr::from($target)), default => $segment, }; @@ -203,7 +203,7 @@ function data_forget(&$target, $key) */ function head($array) { - return reset($array); + return empty($array) ? false : array_first($array); } } @@ -216,7 +216,7 @@ function head($array) */ function last($array) { - return end($array); + return empty($array) ? false : array_last($array); } } diff --git a/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php b/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php index 15e6abe3986d..34f52d340b8a 100644 --- a/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php +++ b/src/Illuminate/Concurrency/Console/InvokeSerializedClosureCommand.php @@ -45,7 +45,9 @@ public function handle() 'successful' => true, 'result' => serialize($this->laravel->call(match (true) { ! is_null($this->argument('code')) => unserialize($this->argument('code')), - isset($_SERVER['LARAVEL_INVOKABLE_CLOSURE']) => unserialize($_SERVER['LARAVEL_INVOKABLE_CLOSURE']), + isset($_SERVER['LARAVEL_INVOKABLE_CLOSURE']) => unserialize( + base64_decode($_SERVER['LARAVEL_INVOKABLE_CLOSURE']) + ), default => fn () => null, })), ])); diff --git a/src/Illuminate/Concurrency/ProcessDriver.php b/src/Illuminate/Concurrency/ProcessDriver.php index bab43e61f309..10c0587d3f2b 100644 --- a/src/Illuminate/Concurrency/ProcessDriver.php +++ b/src/Illuminate/Concurrency/ProcessDriver.php @@ -34,7 +34,9 @@ public function run(Closure|array $tasks): array $results = $this->processFactory->pool(function (Pool $pool) use ($tasks, $command) { foreach (Arr::wrap($tasks) as $key => $task) { $pool->as($key)->path(base_path())->env([ - 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), + 'LARAVEL_INVOKABLE_CLOSURE' => base64_encode( + serialize(new SerializableClosure($task)) + ), ])->command($command); } })->start()->wait(); @@ -68,7 +70,9 @@ public function defer(Closure|array $tasks): DeferredCallback return defer(function () use ($tasks, $command) { foreach (Arr::wrap($tasks) as $task) { $this->processFactory->path(base_path())->env([ - 'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)), + 'LARAVEL_INVOKABLE_CLOSURE' => base64_encode( + serialize(new SerializableClosure($task)) + ), ])->run($command.' 2>&1 &'); } }); diff --git a/src/Illuminate/Config/Repository.php b/src/Illuminate/Config/Repository.php index 19240b42ac93..08801213a0f3 100644 --- a/src/Illuminate/Config/Repository.php +++ b/src/Illuminate/Config/Repository.php @@ -5,6 +5,7 @@ use ArrayAccess; use Illuminate\Contracts\Config\Repository as ConfigContract; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; @@ -177,6 +178,18 @@ public function array(string $key, $default = null): array return $value; } + /** + * Get the specified array configuration value as a collection. + * + * @param string $key + * @param (\Closure():(array|null))|array|null $default + * @return Collection + */ + public function collection(string $key, $default = null): Collection + { + return new Collection($this->array($key, $default)); + } + /** * Set a given configuration value. * diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 07073aab309c..94d1cc2196e7 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -8,7 +8,9 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\ProcessUtils; +use ReflectionClass; use Symfony\Component\Console\Application as SymfonyApplication; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Input\ArrayInput; @@ -145,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 @@ -168,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 */ @@ -177,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(); } @@ -203,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. * @@ -238,12 +258,18 @@ protected function addToParent(SymfonyCommand $command) */ public function resolve($command) { - if (is_subclass_of($command, SymfonyCommand::class) && ($commandName = $command::getDefaultName())) { - foreach (explode('|', $commandName) as $name) { - $this->commandMap[$name] = $command; - } + if (is_subclass_of($command, SymfonyCommand::class)) { + $attribute = (new ReflectionClass($command))->getAttributes(AsCommand::class); + + $commandName = ! empty($attribute) ? $attribute[0]->newInstance()->name : null; - return null; + if (! is_null($commandName)) { + foreach (explode('|', $commandName) as $name) { + $this->commandMap[$name] = $command; + } + + return null; + } } if ($command instanceof Command) { @@ -256,7 +282,7 @@ public function resolve($command) /** * Resolve an array of commands through the application. * - * @param array|mixed $commands + * @param mixed $commands * @return $this */ public function resolveCommands($commands) 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/Concerns/InteractsWithIO.php b/src/Illuminate/Console/Concerns/InteractsWithIO.php index 33a4b726377b..706d9379f7f4 100644 --- a/src/Illuminate/Console/Concerns/InteractsWithIO.php +++ b/src/Illuminate/Console/Concerns/InteractsWithIO.php @@ -206,7 +206,7 @@ public function secret($question, $fallback = true) * @param string $question * @param array $choices * @param string|int|null $default - * @param mixed|null $attempts + * @param mixed $attempts * @param bool $multiple * @return string|array */ @@ -430,6 +430,8 @@ protected function setVerbosity($level) */ protected function parseVerbosity($level = null) { + $level ??= ''; + if (isset($this->verbosityMap[$level])) { $level = $this->verbosityMap[$level]; } elseif (! is_int($level)) { diff --git a/src/Illuminate/Console/Parser.php b/src/Illuminate/Console/Parser.php index d70088e61beb..ae2e99e4af3b 100644 --- a/src/Illuminate/Console/Parser.php +++ b/src/Illuminate/Console/Parser.php @@ -77,20 +77,14 @@ protected static function parseArgument(string $token) { [$token, $description] = static::extractDescription($token); - switch (true) { - case str_ends_with($token, '?*'): - return new InputArgument(trim($token, '?*'), InputArgument::IS_ARRAY, $description); - case str_ends_with($token, '*'): - return new InputArgument(trim($token, '*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description); - case str_ends_with($token, '?'): - return new InputArgument(trim($token, '?'), InputArgument::OPTIONAL, $description); - case preg_match('/(.+)\=\*(.+)/', $token, $matches): - return new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, preg_split('/,\s?/', $matches[2])); - case preg_match('/(.+)\=(.+)/', $token, $matches): - return new InputArgument($matches[1], InputArgument::OPTIONAL, $description, $matches[2]); - default: - return new InputArgument($token, InputArgument::REQUIRED, $description); - } + return match (true) { + str_ends_with($token, '?*') => new InputArgument(trim($token, '?*'), InputArgument::IS_ARRAY, $description), + str_ends_with($token, '*') => new InputArgument(trim($token, '*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description), + str_ends_with($token, '?') => new InputArgument(trim($token, '?'), InputArgument::OPTIONAL, $description), + (bool) preg_match('/(.+)\=\*(.+)/', $token, $matches) => new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, preg_split('/,\s?/', $matches[2])), + (bool) preg_match('/(.+)\=(.+)/', $token, $matches) => new InputArgument($matches[1], InputArgument::OPTIONAL, $description, $matches[2]), + default => new InputArgument($token, InputArgument::REQUIRED, $description), + }; } /** @@ -112,18 +106,13 @@ protected static function parseOption(string $token) $token = $matches[1]; } - switch (true) { - case str_ends_with($token, '='): - return new InputOption(trim($token, '='), $shortcut, InputOption::VALUE_OPTIONAL, $description); - case str_ends_with($token, '=*'): - return new InputOption(trim($token, '=*'), $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description); - case preg_match('/(.+)\=\*(.+)/', $token, $matches): - return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, preg_split('/,\s?/', $matches[2])); - case preg_match('/(.+)\=(.+)/', $token, $matches): - return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL, $description, $matches[2]); - default: - return new InputOption($token, $shortcut, InputOption::VALUE_NONE, $description); - } + return match (true) { + str_ends_with($token, '=') => new InputOption(trim($token, '='), $shortcut, InputOption::VALUE_OPTIONAL, $description), + str_ends_with($token, '=*') => new InputOption(trim($token, '=*'), $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description), + (bool) preg_match('/(.+)\=\*(.+)/', $token, $matches) => new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, preg_split('/,\s?/', $matches[2])), + (bool) preg_match('/(.+)\=(.+)/', $token, $matches) => new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL, $description, $matches[2]), + default => new InputOption($token, $shortcut, InputOption::VALUE_NONE, $description), + }; } /** diff --git a/src/Illuminate/Console/QuestionHelper.php b/src/Illuminate/Console/QuestionHelper.php index 5cbab6f0f1bc..64f8f26dbcd4 100644 --- a/src/Illuminate/Console/QuestionHelper.php +++ b/src/Illuminate/Console/QuestionHelper.php @@ -62,7 +62,7 @@ protected function writePrompt(OutputInterface $output, Question $question): voi if ($question instanceof ChoiceQuestion) { foreach ($question->getChoices() as $key => $value) { - with(new TwoColumnDetail($output))->render($value, $key); + (new TwoColumnDetail($output))->render($value, $key); } } 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/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 944728361a7f..7cda4f054413 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -380,7 +380,7 @@ public function appendOutputTo($location) /** * E-mail the results of the scheduled operation. * - * @param array|mixed $addresses + * @param mixed $addresses * @param bool $onlyIfOutputExists * @return $this * @@ -400,7 +400,7 @@ public function emailOutputTo($addresses, $onlyIfOutputExists = true) /** * E-mail the results of the scheduled operation if it produces output. * - * @param array|mixed $addresses + * @param mixed $addresses * @return $this * * @throws \LogicException @@ -413,7 +413,7 @@ public function emailWrittenOutputTo($addresses) /** * E-mail the results of the scheduled operation if it fails. * - * @param array|mixed $addresses + * @param mixed $addresses * @return $this */ public function emailOutputOnFailure($addresses) diff --git a/src/Illuminate/Console/Scheduling/ManagesAttributes.php b/src/Illuminate/Console/Scheduling/ManagesAttributes.php index 1a18378eb62f..4e9ad38f45cb 100644 --- a/src/Illuminate/Console/Scheduling/ManagesAttributes.php +++ b/src/Illuminate/Console/Scheduling/ManagesAttributes.php @@ -113,7 +113,7 @@ public function user($user) /** * Limit the environments the command should run in. * - * @param array|mixed $environments + * @param mixed $environments * @return $this */ public function environments($environments) diff --git a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php index 619d852e4817..8faf51b72726 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] ); } @@ -499,7 +501,7 @@ public function weekly() /** * Schedule the event to run weekly on a given day and time. * - * @param array|mixed $dayOfWeek + * @param mixed $dayOfWeek * @param string $time * @return $this */ @@ -626,7 +628,7 @@ public function yearlyOn($month = 1, $dayOfMonth = 1, $time = '0:0') /** * Set the days of the week the command should run on. * - * @param array|mixed $days + * @param mixed $days * @return $this */ public function days($days) @@ -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; } @@ -653,7 +655,7 @@ public function timezone($timezone) * Splice the given value into the given position of the expression. * * @param int $position - * @param string $value + * @param string|int $value * @return $this */ protected function spliceIntoPosition($position, $value) diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index 17de97bad8cb..49d0a821c779 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,29 +183,40 @@ 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() : $job::class; } - return $this->name($jobName)->call(function () use ($job, $queue, $connection) { - $job = is_string($job) ? Container::getInstance()->make($job) : $job; + $this->events[] = $event = new CallbackEvent( + $this->eventMutex, function () use ($job, $queue, $connection) { + $job = is_string($job) ? Container::getInstance()->make($job) : $job; + + if ($job instanceof ShouldQueue) { + $this->dispatchToQueue($job, $queue ?? $job->queue, $connection ?? $job->connection); + } else { + $this->dispatchNow($job); + } + }, [], $this->timezone + ); - if ($job instanceof ShouldQueue) { - $this->dispatchToQueue($job, $queue ?? $job->queue, $connection ?? $job->connection); - } else { - $this->dispatchNow($job); - } - }); + $event->name($jobName); + + $this->mergePendingAttributes($event); + + return $event; } /** @@ -297,6 +321,7 @@ public function group(Closure $events) } $this->groupStack[] = $this->attributes; + $this->attributes = null; $events($this); @@ -311,16 +336,16 @@ public function group(Closure $events) */ protected function mergePendingAttributes(Event $event) { - if (isset($this->attributes)) { - $this->attributes->mergeAttributes($event); + if (! empty($this->groupStack)) { + $group = array_last($this->groupStack); - $this->attributes = null; + $group->mergeAttributes($event); } - if (! empty($this->groupStack)) { - $group = end($this->groupStack); + if (isset($this->attributes)) { + $this->attributes->mergeAttributes($event); - $group->mergeAttributes($event); + $this->attributes = null; } } @@ -407,11 +432,13 @@ public function events() /** * Specify the cache store that should be used to store mutexes. * - * @param string $store + * @param \UnitEnum|string $store * @return $this */ public function useCache($store) { + $store = enum_value($store); + if ($this->eventMutex instanceof CacheAware) { $this->eventMutex->useStore($store); } @@ -460,7 +487,7 @@ public function __call($method, $parameters) } if (method_exists(PendingEventAttributes::class, $method)) { - $this->attributes ??= end($this->groupStack) ?: new PendingEventAttributes($this); + $this->attributes ??= $this->groupStack ? clone array_last($this->groupStack) : new PendingEventAttributes($this); return $this->attributes->$method(...$parameters); } 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/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 0bb8f11ab498..9822eafabdeb 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -24,6 +24,7 @@ class ScheduleListCommand extends Command protected $signature = 'schedule:list {--timezone= : The timezone that times should be displayed in} {--next : Sort the listed tasks by their next due date} + {--json : Output the scheduled tasks as JSON} '; /** @@ -53,21 +54,78 @@ public function handle(Schedule $schedule) $events = new Collection($schedule->events()); if ($events->isEmpty()) { - $this->components->info('No scheduled tasks have been defined.'); + if ($this->option('json')) { + $this->output->writeln('[]'); + } else { + $this->components->info('No scheduled tasks have been defined.'); + } return; } + $timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone')); + + $events = $this->sortEvents($events, $timezone); + + $this->option('json') + ? $this->displayJson($events, $timezone) + : $this->displayForCli($events, $timezone); + } + + /** + * Render the scheduled tasks information as JSON. + * + * @param \Illuminate\Support\Collection $events + * @param \DateTimeZone $timezone + * @return void + */ + protected function displayJson(Collection $events, DateTimeZone $timezone) + { + $this->output->writeln($events->map(function ($event) use ($timezone) { + $nextDueDate = $this->getNextDueDateForEvent($event, $timezone); + + $command = $event->command ?? ''; + + if (! $this->output->isVerbose()) { + $command = $event->normalizeCommand($command); + } + + if ($event instanceof CallbackEvent) { + $command = $event->getSummaryForDisplay(); + + if (in_array($command, ['Closure', 'Callback'])) { + $command = 'Closure at: '.$this->getClosureLocation($event); + } + } + + return [ + 'expression' => $event->expression, + 'command' => $command, + 'description' => $event->description ?? null, + 'next_due_date' => $nextDueDate->format('Y-m-d H:i:s P'), + 'next_due_date_human' => $nextDueDate->diffForHumans(), + 'timezone' => $timezone->getName(), + 'has_mutex' => $event->mutex->exists($event), + 'repeat_seconds' => $event->isRepeatable() ? $event->repeatSeconds : null, + ]; + })->values()->toJson()); + } + + /** + * Render the scheduled tasks information formatted for the CLI. + * + * @param \Illuminate\Support\Collection $events + * @param \DateTimeZone $timezone + * @return void + */ + protected function displayForCli(Collection $events, DateTimeZone $timezone) + { $terminalWidth = self::getTerminalWidth(); $expressionSpacing = $this->getCronExpressionSpacing($events); $repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events); - $timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone')); - - $events = $this->sortEvents($events, $timezone); - $events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) { return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone); }); @@ -146,7 +204,7 @@ private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatEx $hasMutex = $event->mutex->exists($event) ? 'Has Mutex › ' : ''; $dots = str_repeat('.', max( - $terminalWidth - mb_strlen($expression.$repeatExpression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0 + $terminalWidth - mb_strwidth($expression.$repeatExpression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0 )); // Highlight the parameters... @@ -194,6 +252,18 @@ private function sortEvents(\Illuminate\Support\Collection $events, DateTimeZone : $events; } + /** + * Render the scheduled tasks information. + * + * @param \Illuminate\Support\Collection $events + * @param \DateTimeZone $timezone + * @return void + */ + protected function display(Collection $events, DateTimeZone $timezone) + { + $this->option('json') ? $this->displayJson($events, $timezone) : $this->displayForCli($events, $timezone); + } + /** * Get the next due date for an event. * diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index 047577372b18..738f88165052 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -22,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. @@ -111,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()) { @@ -126,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 { @@ -140,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(); } @@ -158,7 +162,7 @@ protected function runSingleServerEvent($event) $this->runEvent($event); } else { $this->components->info(sprintf( - 'Skipping [%s], as command already run on another server.', $event->getSummaryForDisplay() + 'Skipping [%s] because the command already ran on another server.', $event->getSummaryForDisplay() )); } } @@ -197,11 +201,11 @@ protected function runEvent($event) round(microtime(true) - $start, 2) )); - if ($event->exitCode !== 0) { + $this->eventsRan = true; + + if ($event->exitCode != 0 && ! $event->runInBackground) { throw new Exception("Scheduled command [{$event->command}] failed with exit code [{$event->exitCode}]."); } - - $this->eventsRan = true; } 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..f91d3dc8b6ba 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php @@ -18,7 +18,9 @@ class ScheduleWorkCommand extends Command * * @var string */ - protected $signature = 'schedule:work {--run-output-file= : The file to direct schedule:run output to}'; + protected $signature = 'schedule:work + {--run-output-file= : The file to direct schedule:run output to} + {--whisper : Do not output message indicating that no jobs were ready to run}'; /** * The console command description. @@ -30,7 +32,7 @@ class ScheduleWorkCommand extends Command /** * Execute the console command. * - * @return void + * @return never */ public function handle() { @@ -43,6 +45,10 @@ public function handle() $command = Application::formatCommandString('schedule:run'); + if ($this->option('whisper')) { + $command .= ' --whisper'; + } + if ($this->option('run-output-file')) { $command .= ' >> '.ProcessUtils::escapeArgument($this->option('run-output-file')).' 2>&1'; } diff --git a/src/Illuminate/Console/Signals.php b/src/Illuminate/Console/Signals.php index 425352594c88..341e0de2681f 100644 --- a/src/Illuminate/Console/Signals.php +++ b/src/Illuminate/Console/Signals.php @@ -51,21 +51,21 @@ public function register($signal, $callback) { $this->previousHandlers[$signal] ??= $this->initializeSignal($signal); - with($this->getHandlers(), function ($handlers) use ($signal) { - $handlers[$signal] ??= $this->initializeSignal($signal); + $handlers = $this->getHandlers(); - $this->setHandlers($handlers); - }); + $handlers[$signal] ??= $this->initializeSignal($signal); + + $this->setHandlers($handlers); $this->registry->register($signal, $callback); - with($this->getHandlers(), function ($handlers) use ($signal) { - $lastHandlerInserted = array_pop($handlers[$signal]); + $handlers = $this->getHandlers(); + + $lastHandlerInserted = array_pop($handlers[$signal]); - array_unshift($handlers[$signal], $lastHandlerInserted); + array_unshift($handlers[$signal], $lastHandlerInserted); - $this->setHandlers($handlers); - }); + $this->setHandlers($handlers); } /** diff --git a/src/Illuminate/Console/View/Components/Ask.php b/src/Illuminate/Console/View/Components/Ask.php index dfd414ad885d..b731408958b0 100644 --- a/src/Illuminate/Console/View/Components/Ask.php +++ b/src/Illuminate/Console/View/Components/Ask.php @@ -10,7 +10,7 @@ class Ask extends Component * Renders the component using the given arguments. * * @param string $question - * @param string $default + * @param string|null $default * @param bool $multiline * @return mixed */ diff --git a/src/Illuminate/Console/View/Components/AskWithCompletion.php b/src/Illuminate/Console/View/Components/AskWithCompletion.php index 103d73071b7a..c28f91236eba 100644 --- a/src/Illuminate/Console/View/Components/AskWithCompletion.php +++ b/src/Illuminate/Console/View/Components/AskWithCompletion.php @@ -11,7 +11,7 @@ class AskWithCompletion extends Component * * @param string $question * @param array|callable $choices - * @param string $default + * @param string|null $default * @return mixed */ public function render($question, $choices, $default = null) diff --git a/src/Illuminate/Console/View/Components/Choice.php b/src/Illuminate/Console/View/Components/Choice.php index ed215527ae8e..1f7818f742fe 100644 --- a/src/Illuminate/Console/View/Components/Choice.php +++ b/src/Illuminate/Console/View/Components/Choice.php @@ -12,7 +12,7 @@ class Choice extends Component * @param string $question * @param array $choices * @param mixed $default - * @param int $attempts + * @param int|null $attempts * @param bool $multiple * @return mixed */ diff --git a/src/Illuminate/Console/View/Components/Component.php b/src/Illuminate/Console/View/Components/Component.php index f515f916ff62..af1ce0ea9aaa 100644 --- a/src/Illuminate/Console/View/Components/Component.php +++ b/src/Illuminate/Console/View/Components/Component.php @@ -103,7 +103,7 @@ protected function mutate($data, $mutators) */ protected function usingQuestionHelper($callable) { - $property = with(new ReflectionClass(OutputStyle::class)) + $property = (new ReflectionClass(OutputStyle::class)) ->getParentClass() ->getProperty('questionHelper'); diff --git a/src/Illuminate/Console/View/Components/Error.php b/src/Illuminate/Console/View/Components/Error.php index 73196cc8440e..bb19ed42f106 100644 --- a/src/Illuminate/Console/View/Components/Error.php +++ b/src/Illuminate/Console/View/Components/Error.php @@ -15,6 +15,6 @@ class Error extends Component */ public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL) { - with(new Line($this->output))->render('error', $string, $verbosity); + (new Line($this->output))->render('error', $string, $verbosity); } } diff --git a/src/Illuminate/Console/View/Components/Factory.php b/src/Illuminate/Console/View/Components/Factory.php index 2929279057ee..2dde02f72384 100644 --- a/src/Illuminate/Console/View/Components/Factory.php +++ b/src/Illuminate/Console/View/Components/Factory.php @@ -56,6 +56,6 @@ public function __call($method, $parameters) 'Console component [%s] not found.', $method ))); - return with(new $component($this->output))->render(...$parameters); + return (new $component($this->output))->render(...$parameters); } } diff --git a/src/Illuminate/Console/View/Components/Info.php b/src/Illuminate/Console/View/Components/Info.php index 765142246fed..ac6652907844 100644 --- a/src/Illuminate/Console/View/Components/Info.php +++ b/src/Illuminate/Console/View/Components/Info.php @@ -15,6 +15,6 @@ class Info extends Component */ public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL) { - with(new Line($this->output))->render('info', $string, $verbosity); + (new Line($this->output))->render('info', $string, $verbosity); } } diff --git a/src/Illuminate/Console/View/Components/Success.php b/src/Illuminate/Console/View/Components/Success.php index 927cafe51e94..9b1419102516 100644 --- a/src/Illuminate/Console/View/Components/Success.php +++ b/src/Illuminate/Console/View/Components/Success.php @@ -15,6 +15,6 @@ class Success extends Component */ public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL) { - with(new Line($this->output))->render('success', $string, $verbosity); + (new Line($this->output))->render('success', $string, $verbosity); } } diff --git a/src/Illuminate/Console/View/Components/Warn.php b/src/Illuminate/Console/View/Components/Warn.php index 20adb1f272b7..d00656ab010e 100644 --- a/src/Illuminate/Console/View/Components/Warn.php +++ b/src/Illuminate/Console/View/Components/Warn.php @@ -15,7 +15,6 @@ class Warn extends Component */ public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL) { - with(new Line($this->output)) - ->render('warn', $string, $verbosity); + (new Line($this->output))->render('warn', $string, $verbosity); } } diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index 82fe11336e8a..4cdc7c576919 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -24,7 +24,7 @@ "laravel/prompts": "^0.3.0", "nunomaduro/termwind": "^2.0", "symfony/console": "^7.2.0", - "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php83": "^1.33", "symfony/process": "^7.2.0" }, "autoload": { diff --git a/src/Illuminate/Container/Attributes/Bind.php b/src/Illuminate/Container/Attributes/Bind.php new file mode 100644 index 000000000000..92bae25479fc --- /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|\UnitEnum $environments + * + * @throws \InvalidArgumentException + */ + public function __construct( + string $concrete, + string|array|UnitEnum $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/Database.php b/src/Illuminate/Container/Attributes/Database.php index 262e3061dea1..0f9eaa7236cd 100644 --- a/src/Illuminate/Container/Attributes/Database.php +++ b/src/Illuminate/Container/Attributes/Database.php @@ -5,6 +5,7 @@ use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; +use UnitEnum; #[Attribute(Attribute::TARGET_PARAMETER)] class Database implements ContextualAttribute @@ -12,7 +13,7 @@ class Database implements ContextualAttribute /** * Create a new class instance. */ - public function __construct(public ?string $connection = null) + public function __construct(public UnitEnum|string|null $connection = null) { } diff --git a/src/Illuminate/Container/Attributes/Give.php b/src/Illuminate/Container/Attributes/Give.php new file mode 100644 index 000000000000..f669c58d1abb --- /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 = []; + + /** + * Whether a class has already been checked for Singleton or Scoped attributes. + * + * @var array + */ + protected $checkedForSingletonOrScopedAttributes = []; + /** * All of the registered rebound callbacks. * @@ -176,6 +194,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 +277,64 @@ 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; + } + + if (($scopedType = $this->getScopedTyped($abstract)) === null) { + return false; + } + + if ($scopedType === 'scoped') { + if (! in_array($abstract, $this->scopedInstances, true)) { + $this->scopedInstances[] = $abstract; + } + } + + return true; + } + + /** + * Determine if a ReflectionClass has scoping attributes applied. + * + * @param ReflectionClass|class-string $reflection + * @return "singleton"|"scoped"|null + */ + protected function getScopedTyped(ReflectionClass|string $reflection): ?string + { + $className = $reflection instanceof ReflectionClass + ? $reflection->getName() + : $reflection; + + if (array_key_exists($className, $this->checkedForSingletonOrScopedAttributes)) { + return $this->checkedForSingletonOrScopedAttributes[$className]; + } + + try { + $reflection = $reflection instanceof ReflectionClass + ? $reflection + : new ReflectionClass($reflection); + } catch (ReflectionException) { + return $this->checkedForSingletonOrScopedAttributes[$className] = null; + } + + $type = null; + + if (! empty($reflection->getAttributes(Singleton::class))) { + $type = 'singleton'; + } elseif (! empty($reflection->getAttributes(Scoped::class))) { + $type = 'scoped'; + } + + return $this->checkedForSingletonOrScopedAttributes[$className] = $type; } /** @@ -332,7 +412,7 @@ protected function getClosure($abstract, $concrete) } return $container->resolve( - $concrete, $parameters, $raiseEvents = false + $concrete, $parameters, raiseEvents: false ); }; } @@ -597,7 +677,7 @@ protected function removeAbstractAlias($searched) * Assign a set of tags to a given binding. * * @param array|string $abstracts - * @param array|mixed ...$tags + * @param mixed ...$tags * @return void */ public function tag($abstracts, $tags) @@ -649,6 +729,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 +1017,69 @@ protected function getConcrete($abstract) return $this->bindings[$abstract]['concrete']; } - return $abstract; + if ($this->environmentResolver === null || + ($this->checkedForAttributeBindings[$abstract] ?? false) || ! is_string($abstract)) { + return $abstract; + } + + return $this->getConcreteBindingFromAttributes($abstract); + } + + /** + * Get the concrete binding for an abstract from the Bind attribute. + * + * @param string $abstract + * @return mixed + */ + protected function getConcreteBindingFromAttributes($abstract) + { + $this->checkedForAttributeBindings[$abstract] = true; + + try { + $reflected = new ReflectionClass($abstract); + } catch (ReflectionException) { + return $abstract; + } + + $bindAttributes = $reflected->getAttributes(Bind::class); + + if ($bindAttributes === []) { + return $abstract; + } + + $concrete = $maybeConcrete = null; + + foreach ($bindAttributes 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; + } + + match ($this->getScopedTyped($reflected)) { + 'scoped' => $this->scoped($abstract, $concrete), + 'singleton' => $this->singleton($abstract, $concrete), + null => $this->bind($abstract, $concrete), + }; + + return $this->bindings[$abstract]['concrete']; } /** @@ -1026,6 +1170,11 @@ public function build($concrete) return $this->notInstantiable($concrete); } + if (is_a($concrete, SelfBuilding::class, true) && + ! in_array($concrete, $this->buildStack, true)) { + return $this->buildSelfBuildingInstance($concrete, $reflector); + } + $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); @@ -1065,6 +1214,34 @@ public function build($concrete) return $instance; } + /** + * Instantiate a concrete instance of the given self building type. + * + * @param \Closure(static, array): TClass|class-string $concrete + * @param \ReflectionClass $reflector + * @return TClass + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function buildSelfBuildingInstance($concrete, $reflector) + { + if (! method_exists($concrete, 'newInstance')) { + throw new BindingResolutionException("No newInstance method exists for [$concrete]."); + } + + $this->buildStack[] = $concrete; + + $instance = $this->call([$concrete, 'newInstance']); + + array_pop($this->buildStack); + + $this->fireAfterResolvingAttributeCallbacks( + $reflector->getAttributes(), $instance + ); + + return $instance; + } + /** * Resolve all of the dependencies from the ReflectionParameters. * @@ -1143,7 +1320,7 @@ protected function getParameterOverride($dependency) */ protected function getLastParameterOverride() { - return count($this->with) ? end($this->with) : []; + return count($this->with) ? array_last($this->with) : []; } /** @@ -1496,6 +1673,16 @@ protected function fireCallbackArray($object, array $callbacks) } } + /** + * Get the name of the binding the container is currently resolving. + * + * @return class-string|string|null + */ + public function currentlyResolving() + { + return array_last($this->buildStack) ?: null; + } + /** * Get the container's bindings. * @@ -1585,6 +1772,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. * @@ -1598,6 +1809,8 @@ public function flush() $this->instances = []; $this->abstractAliases = []; $this->scopedInstances = []; + $this->checkedForAttributeBindings = []; + $this->checkedForSingletonOrScopedAttributes = []; } /** diff --git a/src/Illuminate/Container/ContextualBindingBuilder.php b/src/Illuminate/Container/ContextualBindingBuilder.php index 0f3163f9403a..aeed0cfd1c16 100644 --- a/src/Illuminate/Container/ContextualBindingBuilder.php +++ b/src/Illuminate/Container/ContextualBindingBuilder.php @@ -57,24 +57,26 @@ public function needs($abstract) * Define the implementation for the contextual binding. * * @param \Closure|string|array $implementation - * @return void + * @return $this */ public function give($implementation) { foreach (Util::arrayWrap($this->concrete) as $concrete) { $this->container->addContextualBinding($concrete, $this->needs, $implementation); } + + return $this; } /** * Define tagged services to be used as the implementation for the contextual binding. * * @param string $tag - * @return void + * @return $this */ public function giveTagged($tag) { - $this->give(function ($container) use ($tag) { + return $this->give(function ($container) use ($tag) { $taggedServices = $container->tagged($tag); return is_array($taggedServices) ? $taggedServices : iterator_to_array($taggedServices); @@ -86,10 +88,10 @@ public function giveTagged($tag) * * @param string $key * @param mixed $default - * @return void + * @return $this */ public function giveConfig($key, $default = null) { - $this->give(fn ($container) => $container->get('config')->get($key, $default)); + return $this->give(fn ($container) => $container->get('config')->get($key, $default)); } } diff --git a/src/Illuminate/Container/composer.json b/src/Illuminate/Container/composer.json index 16d737f2a216..cf80aac08278 100755 --- a/src/Illuminate/Container/composer.json +++ b/src/Illuminate/Container/composer.json @@ -16,7 +16,17 @@ "require": { "php": "^8.2", "illuminate/contracts": "^12.0", - "psr/container": "^1.1.1|^2.0.1" + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" }, "provide": { "psr/container-implementation": "1.1|2.0" diff --git a/src/Illuminate/Contracts/Auth/Access/Authorizable.php b/src/Illuminate/Contracts/Auth/Access/Authorizable.php index cedeb6ea3440..6b8a0441c92e 100644 --- a/src/Illuminate/Contracts/Auth/Access/Authorizable.php +++ b/src/Illuminate/Contracts/Auth/Access/Authorizable.php @@ -8,7 +8,7 @@ interface Authorizable * Determine if the entity has a given ability. * * @param iterable|string $abilities - * @param array|mixed $arguments + * @param mixed $arguments * @return bool */ public function can($abilities, $arguments = []); diff --git a/src/Illuminate/Contracts/Auth/Access/Gate.php b/src/Illuminate/Contracts/Auth/Access/Gate.php index 4bafab3f4dc5..29ab2377d9c9 100644 --- a/src/Illuminate/Contracts/Auth/Access/Gate.php +++ b/src/Illuminate/Contracts/Auth/Access/Gate.php @@ -15,7 +15,7 @@ public function has($ability); /** * Define a new ability. * - * @param string $ability + * @param \UnitEnum|string $ability * @param callable|string $callback * @return $this */ @@ -59,8 +59,8 @@ public function after(callable $callback); /** * Determine if all of the given abilities should be granted for the current user. * - * @param iterable|string $ability - * @param array|mixed $arguments + * @param iterable|\UnitEnum|string $ability + * @param mixed $arguments * @return bool */ public function allows($ability, $arguments = []); @@ -68,8 +68,8 @@ public function allows($ability, $arguments = []); /** * Determine if any of the given abilities should be denied for the current user. * - * @param iterable|string $ability - * @param array|mixed $arguments + * @param iterable|\UnitEnum|string $ability + * @param mixed $arguments * @return bool */ public function denies($ability, $arguments = []); @@ -77,8 +77,8 @@ public function denies($ability, $arguments = []); /** * Determine if all of the given abilities should be granted for the current user. * - * @param iterable|string $abilities - * @param array|mixed $arguments + * @param iterable|\UnitEnum|string $abilities + * @param mixed $arguments * @return bool */ public function check($abilities, $arguments = []); @@ -86,8 +86,8 @@ public function check($abilities, $arguments = []); /** * Determine if any one of the given abilities should be granted for the current user. * - * @param iterable|string $abilities - * @param array|mixed $arguments + * @param iterable|\UnitEnum|string $abilities + * @param mixed $arguments * @return bool */ public function any($abilities, $arguments = []); @@ -95,8 +95,8 @@ public function any($abilities, $arguments = []); /** * Determine if the given ability should be granted for the current user. * - * @param string $ability - * @param array|mixed $arguments + * @param \UnitEnum|string $ability + * @param mixed $arguments * @return \Illuminate\Auth\Access\Response * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -106,8 +106,8 @@ public function authorize($ability, $arguments = []); /** * Inspect the user for the given ability. * - * @param string $ability - * @param array|mixed $arguments + * @param \UnitEnum|string $ability + * @param mixed $arguments * @return \Illuminate\Auth\Access\Response */ public function inspect($ability, $arguments = []); @@ -116,7 +116,7 @@ public function inspect($ability, $arguments = []); * Get the raw result from the authorization callback. * * @param string $ability - * @param array|mixed $arguments + * @param mixed $arguments * @return mixed * * @throws \Illuminate\Auth\Access\AuthorizationException 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/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 23bc60434e49..9874727d26c9 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -7,6 +7,9 @@ use RuntimeException; use Throwable; +/** + * @mixin \Illuminate\Database\Connection + */ trait ManagesTransactions { /** @@ -148,7 +151,7 @@ protected function createTransaction() $this->reconnectIfMissingConnection(); try { - $this->getPdo()->beginTransaction(); + $this->executeBeginTransactionStatement(); } catch (Throwable $e) { $this->handleBeginTransactionException($e); } @@ -184,7 +187,7 @@ protected function handleBeginTransactionException(Throwable $e) if ($this->causedByLostConnection($e)) { $this->reconnect(); - $this->getPdo()->beginTransaction(); + $this->executeBeginTransactionStatement(); } else { throw $e; } @@ -350,4 +353,21 @@ public function afterCommit($callback) throw new RuntimeException('Transactions Manager has not been set.'); } + + /** + * Execute the callback after a transaction rolls back. + * + * @param callable $callback + * @return void + * + * @throws \RuntimeException + */ + public function afterRollBack($callback) + { + if ($this->transactionsManager) { + return $this->transactionsManager->addCallbackForRollback($callback); + } + + throw new RuntimeException('Transactions Manager has not been set.'); + } } diff --git a/src/Illuminate/Database/ConcurrencyErrorDetector.php b/src/Illuminate/Database/ConcurrencyErrorDetector.php new file mode 100644 index 000000000000..3a5cb1dcd0a6 --- /dev/null +++ b/src/Illuminate/Database/ConcurrencyErrorDetector.php @@ -0,0 +1,39 @@ +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', + 'Record has changed since last read in table', + ]); + } +} diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index a883e3edb22e..d8af11f1fb29 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); } /** @@ -367,7 +369,7 @@ public function scalar($query, $bindings = [], $useReadPdo = true) throw new MultipleColumnsSelectedException; } - return reset($record); + return array_first($record); } /** @@ -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) { @@ -860,7 +862,7 @@ public function logQuery($query, $bindings, $time = null) } /** - * Get the elapsed time since a given starting point. + * Get the elapsed time in milliseconds since a given starting point. * * @param float $start * @return float @@ -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, \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) @@ -1468,6 +1470,16 @@ public function unsetEventDispatcher() $this->events = null; } + /** + * Run the statement to start a new transaction. + * + * @return void + */ + protected function executeBeginTransactionStatement() + { + $this->getPdo()->beginTransaction(); + } + /** * Set the transaction manager instance on the connection. * @@ -1504,7 +1516,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/Connectors/SQLiteConnector.php b/src/Illuminate/Database/Connectors/SQLiteConnector.php index 2e2ed8758919..858549ec55de 100755 --- a/src/Illuminate/Database/Connectors/SQLiteConnector.php +++ b/src/Illuminate/Database/Connectors/SQLiteConnector.php @@ -20,6 +20,7 @@ public function connect(array $config) $connection = $this->createConnection("sqlite:{$path}", $config, $options); + $this->configurePragmas($connection, $config); $this->configureForeignKeyConstraints($connection, $config); $this->configureBusyTimeout($connection, $config); $this->configureJournalMode($connection, $config); @@ -62,6 +63,24 @@ protected function parseDatabasePath(string $path): string return $path; } + /** + * Set miscellaneous user-configured pragmas. + * + * @param \PDO $connection + * @param array $config + * @return void + */ + protected function configurePragmas($connection, array $config): void + { + if (! isset($config['pragmas'])) { + return; + } + + foreach ($config['pragmas'] as $pragma => $value) { + $connection->prepare("pragma {$pragma} = {$value}")->execute(); + } + } + /** * Enable or disable foreign key constraints if configured. * 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/Migrations/MigrateMakeCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php index c9494c5d5c44..ac5077f58d79 100644 --- a/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php @@ -112,6 +112,10 @@ protected function writeMigration($name, $table, $create) $name, $this->getMigrationPath(), $table, $create ); + if (windows_os()) { + $file = str_replace('/', '\\', $file); + } + $this->components->info(sprintf('Migration [%s] created successfully.', $file)); } 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/TableCommand.php b/src/Illuminate/Database/Console/TableCommand.php index fde40a78f8a3..94b313f57849 100644 --- a/src/Illuminate/Database/Console/TableCommand.php +++ b/src/Illuminate/Database/Console/TableCommand.php @@ -47,7 +47,17 @@ public function handle(ConnectionResolverInterface $connections) array_keys($tables) ); - $table = $tables[$tableName] ?? Arr::first($tables, fn ($table) => $table['name'] === $tableName); + $table = $tables[$tableName] ?? (new Collection($tables))->when( + Arr::wrap($connection->getSchemaBuilder()->getCurrentSchemaListing() + ?? $connection->getSchemaBuilder()->getCurrentSchemaName()), + fn (Collection $collection, array $currentSchemas) => $collection->sortBy( + function (array $table) use ($currentSchemas) { + $index = array_search($table['schema'], $currentSchemas); + + return $index === false ? PHP_INT_MAX : $index; + } + ) + )->firstWhere('name', $tableName); if (! $table) { $this->components->warn("Table [{$tableName}] doesn't exist."); 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..2b8a0cc000f2 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,14 +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(); - - [$database, $type] = $this->parseConnectionName($name); + [$database, $type] = $this->parseConnectionName($name = enum_value($name) ?: $this->getDefaultConnection()); // If we haven't created this connection, we'll create it based on the config // provided in the application. Once we've created the connections we will @@ -116,9 +116,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; @@ -141,7 +139,7 @@ public static function calculateDynamicConnectionName(array $config) /** * Get a database connection instance from the given configuration. * - * @param string $name + * @param \UnitEnum|string $name * @param array $config * @param bool $force * @return \Illuminate\Database\ConnectionInterface @@ -149,7 +147,7 @@ public static function calculateDynamicConnectionName(array $config) public function connectUsing(string $name, array $config, bool $force = false) { if ($force) { - $this->purge($name); + $this->purge($name = enum_value($name)); } if (isset($this->connections[$name])) { @@ -173,8 +171,6 @@ public function connectUsing(string $name, array $config, bool $force = false) */ protected function parseConnectionName($name) { - $name = $name ?: $this->getDefaultConnection(); - return Str::endsWith($name, ['::read', '::write']) ? explode('::', $name, 2) : [$name, null]; @@ -217,8 +213,6 @@ protected function makeConnection($name) */ protected function configuration($name) { - $name = $name ?: $this->getDefaultConnection(); - $connections = $this->app['config']['database.connections']; $config = $this->dynamicConnectionConfigurations[$name] ?? Arr::get($connections, $name); @@ -299,14 +293,12 @@ protected function setPdoForType(Connection $connection, $type = null) /** * Disconnect from the given database and remove from local cache. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return void */ public function purge($name = null) { - $name = $name ?: $this->getDefaultConnection(); - - $this->disconnect($name); + $this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection()); unset($this->connections[$name]); } @@ -314,12 +306,12 @@ public function purge($name = null) /** * Disconnect from the given database. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return void */ public function disconnect($name = null) { - if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) { + if (isset($this->connections[$name = enum_value($name) ?: $this->getDefaultConnection()])) { $this->connections[$name]->disconnect(); } } @@ -327,12 +319,12 @@ public function disconnect($name = null) /** * Reconnect to the given database. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return \Illuminate\Database\Connection */ public function reconnect($name = null) { - $this->disconnect($name = $name ?: $this->getDefaultConnection()); + $this->disconnect($name = enum_value($name) ?: $this->getDefaultConnection()); if (! isset($this->connections[$name])) { return $this->connection($name); @@ -344,7 +336,7 @@ public function reconnect($name = null) /** * Set the default database connection for the callback execution. * - * @param string $name + * @param \UnitEnum|string $name * @param callable $callback * @return mixed */ @@ -352,11 +344,13 @@ public function usingConnection($name, callable $callback) { $previousName = $this->getDefaultConnection(); - $this->setDefaultConnection($name); + $this->setDefaultConnection($name = enum_value($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..82ae4568fc51 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; + }); } /** @@ -86,6 +96,10 @@ protected function registerConnectionServices() */ protected function registerFakerGenerator() { + if (! class_exists(FakerGenerator::class)) { + return; + } + $this->app->singleton(FakerGenerator::class, function ($app, $parameters) { $locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US'); 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/Attributes/UseResource.php b/src/Illuminate/Database/Eloquent/Attributes/UseResource.php new file mode 100644 index 000000000000..a1cbc48f3a90 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/UseResource.php @@ -0,0 +1,18 @@ + $class + */ + public function __construct(public string $class) + { + } +} diff --git a/src/Illuminate/Database/Eloquent/Attributes/UseResourceCollection.php b/src/Illuminate/Database/Eloquent/Attributes/UseResourceCollection.php new file mode 100644 index 000000000000..c17e1f1768dd --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/UseResourceCollection.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..d32a17b207ef 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', @@ -241,6 +242,21 @@ public function withoutGlobalScopes(?array $scopes = null) return $this; } + /** + * Remove all global scopes except the given scopes. + * + * @param array $scopes + * @return $this + */ + public function withoutGlobalScopesExcept(array $scopes = []) + { + $this->withoutGlobalScopes( + array_diff(array_keys($this->scopes), $scopes) + ); + + return $this; + } + /** * Get an array of global scopes that were removed from the query. * @@ -309,6 +325,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. * @@ -491,7 +522,7 @@ public function fillForInsert(array $values) return []; } - if (! is_array(reset($values))) { + if (! is_array(array_first($values))) { $values = [$values]; } @@ -1118,7 +1149,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(), @@ -1249,12 +1280,12 @@ public function upsert(array $values, $uniqueBy, $update = null) return 0; } - if (! is_array(reset($values))) { + if (! is_array(array_first($values))) { $values = [$values]; } if (is_null($update)) { - $update = array_keys(reset($values)); + $update = array_keys(array_first($values)); } return $this->toBase()->upsert( @@ -1350,7 +1381,7 @@ protected function addUpdatedAtColumn(array $values) $segments = preg_split('/\s+as\s+/i', $this->query->from); - $qualifiedColumn = end($segments).'.'.$column; + $qualifiedColumn = array_last($segments).'.'.$column; $values[$qualifiedColumn] = Arr::get($values, $qualifiedColumn, $values[$column]); diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php index e36b13df2184..5f7d0845c8ff 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php @@ -80,7 +80,7 @@ public static function of($map) * Specify the collection type for the cast. * * @param class-string $class - * @param array{class-string, string}|class-string $map + * @param array{class-string, string}|class-string|null $map * @return string */ public static function using($class, $map = null) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php index b5912fa20b10..ab122ef3af40 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php @@ -79,7 +79,7 @@ public static function of($map) * Specify the collection for the cast. * * @param class-string $class - * @param array{class-string, string}|class-string $map + * @param array{class-string, string}|class-string|null $map * @return string */ public static function using($class, $map = null) 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..e3a67bc152a7 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; @@ -221,28 +222,28 @@ public function loadMissing($relations) $relations = func_get_args(); } - foreach ($relations as $key => $value) { - if (is_numeric($key)) { - $key = $value; - } + if ($this->isNotEmpty()) { + $query = $this->first()->newQueryWithoutRelationships()->with($relations); - $segments = explode('.', explode(':', $key)[0]); + foreach ($query->getEagerLoads() as $key => $value) { + $segments = explode('.', explode(':', $key)[0]); - if (str_contains($key, ':')) { - $segments[count($segments) - 1] .= ':'.explode(':', $key)[1]; - } + if (str_contains($key, ':')) { + $segments[count($segments) - 1] .= ':'.explode(':', $key)[1]; + } - $path = []; + $path = []; - foreach ($segments as $segment) { - $path[] = [$segment => $segment]; - } + foreach ($segments as $segment) { + $path[] = [$segment => $segment]; + } - if (is_callable($value)) { - $path[count($segments) - 1][end($segments)] = $value; - } + if (is_callable($value)) { + $path[count($segments) - 1][array_last($segments)] = $value; + } - $this->loadMissingRelation($this, $path); + $this->loadMissingRelation($this, $path); + } } return $this; @@ -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..759ba7a6dbb4 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) { @@ -214,7 +216,7 @@ public function isGuarded($key) */ protected function isGuardableColumn($key) { - if ($this->hasSetMutator($key) || $this->hasAttributeSetMutator($key)) { + if ($this->hasSetMutator($key) || $this->hasAttributeSetMutator($key) || $this->isClassCastable($key)) { return true; } @@ -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..7591252b5148 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -41,6 +41,7 @@ use ReflectionMethod; use ReflectionNamedType; use RuntimeException; +use Stringable; use ValueError; use function Illuminate\Support\enum_value; @@ -50,24 +51,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 +210,7 @@ protected function initializeHasAttributes() /** * Convert the model's attributes to an array. * - * @return array + * @return array */ public function attributesToArray() { @@ -237,8 +245,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 +266,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 +294,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 +349,7 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt /** * Get an attribute array of all arrayable attributes. * - * @return array + * @return array */ protected function getArrayableAttributes() { @@ -783,6 +791,15 @@ protected function ensureCastsAreStringValues($casts) { foreach ($casts as $attribute => $cast) { $casts[$attribute] = match (true) { + is_object($cast) => value(function () use ($cast, $attribute) { + if ($cast instanceof Stringable) { + return (string) $cast; + } + + throw new InvalidArgumentException( + "The cast object for the {$attribute} attribute must implement Stringable." + ); + }), is_array($cast) => value(function () use ($cast) { if (count($cast) === 1) { return $cast[0]; @@ -983,6 +1000,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 +1446,7 @@ public static function encryptUsing($encrypter) * * @return \Illuminate\Contracts\Encryption\Encrypter */ - protected static function currentEncrypter() + public static function currentEncrypter() { return static::$encrypter ?? Crypt::getFacadeRoot(); } @@ -1756,6 +1788,10 @@ protected function isEnumCastable($key) return false; } + if (is_subclass_of($castType, Castable::class)) { + return false; + } + return enum_exists($castType); } @@ -1793,6 +1829,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 +2002,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 +2016,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 +2036,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 +2046,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 +2063,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 +2107,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 +2131,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 +2139,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 +2152,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 +2167,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 +2178,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 +2191,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 +2219,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 +2237,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 +2247,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 +2310,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 +2364,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) @@ -2334,6 +2399,19 @@ public function setAppends(array $appends) return $this; } + /** + * Merge new appended attributes with existing appended attributes on the model. + * + * @param array $appends + * @return $this + */ + public function mergeAppends(array $appends) + { + $this->appends = array_values(array_unique(array_merge($this->appends, $appends))); + + return $this; + } + /** * Return whether the accessor attribute has been appended. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php index fa654de966d4..256129e56d67 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php @@ -156,7 +156,7 @@ public function setObservableEvents(array $observables) /** * Add an observable event name. * - * @param array|mixed $observables + * @param mixed $observables * @return void */ public function addObservableEvents($observables) @@ -169,7 +169,7 @@ public function addObservableEvents($observables) /** * Remove an observable event name. * - * @param array|mixed $observables + * @param mixed $observables * @return void */ public function removeObservableEvents($observables) @@ -231,7 +231,7 @@ protected function fireModelEvent($event, $halt = true) * * @param string $event * @param string $method - * @return mixed|null + * @return mixed */ protected function fireCustomModelEvent($event, $method) { diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php index df69409ec835..635ac8d1fe9d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -8,6 +8,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use InvalidArgumentException; +use ReflectionAttribute; use ReflectionClass; trait HasGlobalScopes @@ -31,8 +32,13 @@ public static function resolveGlobalScopeAttributes() { $reflectionClass = new ReflectionClass(static::class); - return (new Collection($reflectionClass->getAttributes(ScopedBy::class))) - ->map(fn ($attribute) => $attribute->getArguments()) + $attributes = (new Collection($reflectionClass->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF))); + + foreach ($reflectionClass->getTraits() as $trait) { + $attributes->push(...$trait->getAttributes(ScopedBy::class, ReflectionAttribute::IS_INSTANCEOF)); + } + + return $attributes->map(fn ($attribute) => $attribute->getArguments()) ->flatten() ->all(); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php index c124fc60324e..dde5ea035297 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php @@ -41,6 +41,19 @@ public function setHidden(array $hidden) return $this; } + /** + * Merge new hidden attributes with existing hidden attributes on the model. + * + * @param array $hidden + * @return $this + */ + public function mergeHidden(array $hidden) + { + $this->hidden = array_values(array_unique(array_merge($this->hidden, $hidden))); + + return $this; + } + /** * Get the visible attributes for the model. * @@ -64,6 +77,19 @@ public function setVisible(array $visible) return $this; } + /** + * Merge new visible attributes with existing visible attributes on the model. + * + * @param array $visible + * @return $this + */ + public function mergeVisible(array $visible) + { + $this->visible = array_values(array_unique(array_merge($this->visible, $visible))); + + return $this; + } + /** * Make the given, typically hidden, attributes visible. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index f9f21536d1fc..d51a862f17d2 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. @@ -803,7 +812,7 @@ public function whereAttachedTo($related, $relationshipName = null, $boolean = ' $this->has( $relationshipName, boolean: $boolean, - callback: fn (Builder $query) => $query->whereKey($relatedCollection), + callback: fn (Builder $query) => $query->whereKey($relatedCollection->pluck($related->getKeyName())), ); return $this; @@ -828,7 +837,7 @@ public function orWhereAttachedTo($related, $relationshipName = null) * * @param mixed $relations * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $function + * @param string|null $function * @return $this */ public function withAggregate($relations, $column, $function = null) diff --git a/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php b/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php index 578de7d0a86f..35e3f98cf9af 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php +++ b/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php @@ -2,9 +2,11 @@ namespace Illuminate\Database\Eloquent\Concerns; +use Illuminate\Database\Eloquent\Attributes\UseResource; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Str; use LogicException; +use ReflectionClass; trait TransformsToResource { @@ -13,8 +15,6 @@ trait TransformsToResource * * @param class-string<\Illuminate\Http\Resources\Json\JsonResource>|null $resourceClass * @return \Illuminate\Http\Resources\Json\JsonResource - * - * @throws \Throwable */ public function toResource(?string $resourceClass = null): JsonResource { @@ -29,11 +29,15 @@ public function toResource(?string $resourceClass = null): JsonResource * Guess the resource class for the model. * * @return \Illuminate\Http\Resources\Json\JsonResource - * - * @throws \Throwable */ protected function guessResource(): JsonResource { + $resourceClass = $this->resolveResourceFromAttribute(static::class); + + if ($resourceClass !== null && class_exists($resourceClass)) { + return $resourceClass::make($this); + } + foreach (static::guessResourceName() as $resourceClass) { if (is_string($resourceClass) && class_exists($resourceClass)) { return $resourceClass::make($this); @@ -46,7 +50,7 @@ protected function guessResource(): JsonResource /** * Guess the resource class name for the model. * - * @return array> + * @return array{class-string<\Illuminate\Http\Resources\Json\JsonResource>, class-string<\Illuminate\Http\Resources\Json\JsonResource>} */ public static function guessResourceName(): array { @@ -71,4 +75,23 @@ class_basename($modelClass) return [$potentialResource.'Resource', $potentialResource]; } + + /** + * Get the resource class from the class attribute. + * + * @param class-string<\Illuminate\Http\Resources\Json\JsonResource> $class + * @return class-string<*>|null + */ + protected function resolveResourceFromAttribute(string $class): ?string + { + if (! class_exists($class)) { + return null; + } + + $attributes = (new ReflectionClass($class))->getAttributes(UseResource::class); + + return $attributes !== [] + ? $attributes[0]->newInstance()->class + : null; + } } diff --git a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php index fd350e6fce6c..5498dc856516 100644 --- a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php @@ -50,9 +50,13 @@ public function __construct($factory, $pivot, $relationship) */ public function createFor(Model $model) { - $relationship = $model->{$this->relationship}(); + $factoryInstance = $this->factory instanceof Factory; - Collection::wrap($this->factory instanceof Factory ? $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { + if ($factoryInstance) { + $relationship = $model->{$this->relationship}(); + } + + Collection::wrap($factoryInstance ? $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { $model->{$this->relationship}()->attach( $attachable, is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index a52d840f421e..ed80b9fe5305 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; @@ -17,6 +16,9 @@ use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\Macroable; use Throwable; +use UnitEnum; + +use function Illuminate\Support\enum_value; /** * @template TModel of \Illuminate\Database\Eloquent\Model @@ -92,10 +94,17 @@ 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. * - * @var string|null + * @var \UnitEnum|string|null */ protected $connection; @@ -134,6 +143,13 @@ abstract class Factory */ protected static $factoryNameResolver; + /** + * Whether to expand relationships by default. + * + * @var bool + */ + protected static $expandRelationshipsByDefault = true; + /** * Create a new factory instance. * @@ -143,9 +159,10 @@ abstract class Factory * @param \Illuminate\Support\Collection|null $for * @param \Illuminate\Support\Collection|null $afterMaking * @param \Illuminate\Support\Collection|null $afterCreating - * @param string|null $connection + * @param \UnitEnum|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 +173,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 +185,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 +414,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); + } - $instances = $this->newModel()->newCollection(array_map(function () use ($parent) { - return $this->makeInstance($parent); - }, range(1, $this->count))); + if ($this->count === null) { + return tap($this->makeInstance($parent), function ($instance) { + $this->callAfterMaking(new Collection([$instance])); + }); + } + + if ($this->count < 1) { + return $this->newModel()->newCollection(); + } - $this->callAfterMaking($instances); + $instances = $this->newModel()->newCollection(array_map(function () use ($parent) { + return $this->makeInstance($parent); + }, range(1, $this->count))); - return $instances; + $this->callAfterMaking($instances); + + return $instances; + } finally { + Model::automaticallyEagerLoadRelationships($autoEagerLoadingEnabled); + } } /** @@ -489,9 +518,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 +538,7 @@ protected function expandAttributes(array $definition) $attribute = $attribute($definition); } - $attribute = $evaluateRelations($attribute); + $attribute = $evaluateRelations($attribute, $key); $definition[$key] = $attribute; @@ -518,7 +550,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 +565,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 +790,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]); } /** @@ -772,16 +805,16 @@ public function withoutParents() */ public function getConnectionName() { - return $this->connection; + return enum_value($this->connection); } /** * Specify the database connection that should be used to generate models. * - * @param string $connection + * @param \UnitEnum|string $connection * @return static */ - public function connection(string $connection) + public function connection(UnitEnum|string $connection) { return $this->newInstance(['connection' => $connection]); } @@ -804,6 +837,7 @@ protected function newInstance(array $arguments = []) 'connection' => $this->connection, 'recycle' => $this->recycle, 'expandRelationships' => $this->expandRelationships, + 'excludeRelationships' => $this->excludeRelationships, ], $arguments))); } @@ -896,13 +930,37 @@ 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. * - * @return \Faker\Generator + * @return \Faker\Generator|null */ protected function withFaker() { + if (! class_exists(Generator::class)) { + return; + } + return Container::getInstance()->make(Generator::class); } @@ -956,6 +1014,7 @@ public static function flushState() static::$modelNameResolvers = []; static::$factoryNameResolver = null; static::$namespace = 'Database\\Factories\\'; + static::$expandRelationshipsByDefault = true; } /** @@ -971,7 +1030,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/Factories/Sequence.php b/src/Illuminate/Database/Eloquent/Factories/Sequence.php index 11971eced7da..4d350d2ad193 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Sequence.php +++ b/src/Illuminate/Database/Eloquent/Factories/Sequence.php @@ -51,11 +51,13 @@ public function count(): int /** * Get the next value in the sequence. * + * @param array $attributes + * @param \Illuminate\Database\Eloquent\Model|null $parent * @return mixed */ - public function __invoke() + public function __invoke($attributes = [], $parent = null) { - return tap(value($this->sequence[$this->index % $this->count], $this), function () { + return tap(value($this->sequence[$this->index % $this->count], $this, $attributes, $parent), function () { $this->index = $this->index + 1; }); } 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..90260a57ca32 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,9 +29,12 @@ use JsonException; use JsonSerializable; use LogicException; +use ReflectionClass; use ReflectionMethod; use Stringable; +use function Illuminate\Support\enum_value; + abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, Stringable, UrlRoutable { use Concerns\HasAttributes, @@ -48,7 +54,7 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt /** * The connection name for the model. * - * @var string|null + * @var \UnitEnum|string|null */ protected $connection; @@ -248,6 +254,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 +292,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 +364,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 +600,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 +650,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 +689,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,8 +718,8 @@ public function newInstance($attributes = [], $exists = false) /** * Create a new model instance that is existing. * - * @param array $attributes - * @param string|null $connection + * @param array $attributes + * @param \UnitEnum|string|null $connection * @return static */ public function newFromBuilder($attributes = [], $connection = null) @@ -696,7 +728,7 @@ public function newFromBuilder($attributes = [], $connection = null) $model->setRawAttributes((array) $attributes, true); - $model->setConnection($connection ?: $this->getConnectionName()); + $model->setConnection($connection ?? $this->getConnectionName()); $model->fireModelEvent('retrieved', false); @@ -706,7 +738,7 @@ public function newFromBuilder($attributes = [], $connection = null) /** * Begin querying the model on a given connection. * - * @param string|null $connection + * @param \UnitEnum|string|null $connection * @return \Illuminate\Database\Eloquent\Builder */ public static function on($connection = null) @@ -714,11 +746,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 +1077,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 +1093,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 +1111,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 +1428,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 +1679,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 +1717,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 @@ -1753,6 +1802,19 @@ public function toJson($options = 0) return $json; } + /** + * Convert the model instance to pretty print formatted JSON. + * + * @param int $options + * @return string + * + * @throws \Illuminate\Database\Eloquent\JsonEncodingException + */ + public function toPrettyJson(int $options = 0) + { + return $this->toJson(JSON_PRETTY_PRINT | $options); + } + /** * Convert the object into something JSON serializable. * @@ -1891,13 +1953,13 @@ public function getConnection() */ public function getConnectionName() { - return $this->connection; + return enum_value($this->connection); } /** * Set the connection associated with the model. * - * @param string|null $name + * @param \UnitEnum|string|null $name * @return $this */ public function setConnection($name) @@ -1910,7 +1972,7 @@ public function setConnection($name) /** * Resolve a connection instance. * - * @param string|null $connection + * @param \UnitEnum|string|null $connection * @return \Illuminate\Database\Connection */ public static function resolveConnection($connection = null) @@ -2266,6 +2328,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 +2485,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/ModelInspector.php b/src/Illuminate/Database/Eloquent/ModelInspector.php index b0db2130c0a3..ea11b803f611 100644 --- a/src/Illuminate/Database/Eloquent/ModelInspector.php +++ b/src/Illuminate/Database/Eloquent/ModelInspector.php @@ -2,7 +2,6 @@ namespace Illuminate\Database\Eloquent; -use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection as BaseCollection; @@ -60,7 +59,7 @@ public function __construct(Application $app) * @param string|null $connection * @return array{"class": class-string<\Illuminate\Database\Eloquent\Model>, database: string, table: string, policy: class-string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>} * - * @throws BindingResolutionException + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function inspect($model, $connection = null) { @@ -243,7 +242,7 @@ protected function getEvents($model) * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Support\Collection * - * @throws BindingResolutionException + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function getObservers($model) { @@ -386,7 +385,7 @@ protected function attributeIsHidden($attribute, $model) * * @param array $column * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed|null + * @return mixed */ protected function getColumnDefault($column, $model) { 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/BelongsTo.php b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php index e145040a92c9..24c61f63c036 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php @@ -168,8 +168,8 @@ public function match(array $models, EloquentCollection $results, $relation) foreach ($models as $model) { $attribute = $this->getDictionaryKey($this->getForeignKeyFrom($model)); - if (isset($dictionary[$attribute])) { - $model->setRelation($relation, $dictionary[$attribute]); + if (isset($dictionary[$attribute ?? ''])) { + $model->setRelation($relation, $dictionary[$attribute ?? '']); } } @@ -248,7 +248,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index e125a760410b..23c4683b0b83 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; } @@ -1445,7 +1445,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $parentQuery, $columns = ['*']) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 8de013a1a38a..dd324d09a672 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} @@ -592,7 +596,7 @@ public function newPivotQuery() /** * Set the columns on the pivot table to retrieve. * - * @param array|mixed $columns + * @param mixed $columns * @return $this */ public function withPivot($columns) diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index 7451491cbaf9..c4c684d8e234 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -291,7 +291,7 @@ public function updateOrCreate(array $attributes, array $values = []) */ public function upsert(array $values, $uniqueBy, $update = null) { - if (! empty($values) && ! is_array(reset($values))) { + if (! empty($values) && ! is_array(array_first($values))) { $values = [$values]; } @@ -500,7 +500,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) @@ -581,7 +581,7 @@ public function getForeignKeyName() { $segments = explode('.', $this->getQualifiedForeignKeyName()); - return end($segments); + return array_last($segments); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrManyThrough.php index 97c011d6cefb..4be008110d82 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; } @@ -469,7 +468,7 @@ public function get($columns = ['*']) * @param int|null $perPage * @param array $columns * @param string $pageName - * @param int $page + * @param int|null $page * @return \Illuminate\Pagination\LengthAwarePaginator */ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) @@ -703,7 +702,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) @@ -728,7 +727,7 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*']) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index 6d6a34c31f38..7c32befcdf1d 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -116,7 +116,7 @@ protected function setForeignAttributesForCreate(Model $model) */ public function upsert(array $values, $uniqueBy, $update = null) { - if (! empty($values) && ! is_array(reset($values))) { + if (! empty($values) && ! is_array(array_first($values))) { $values = [$values]; } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 162ebec1777b..91bbb4b72d3d 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -121,13 +121,14 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, } /** - * Get the pivot models that are currently attached. + * Get the pivot models that are currently attached, filtered by related model keys. * + * @param mixed $ids * @return \Illuminate\Support\Collection */ - protected function getCurrentlyAttachedPivots() + protected function getCurrentlyAttachedPivotsForIds($ids = null) { - return parent::getCurrentlyAttachedPivots()->map(function ($record) { + return parent::getCurrentlyAttachedPivotsForIds($ids)->map(function ($record) { return $record instanceof MorphPivot ? $record->setMorphType($this->morphType) ->setMorphClass($this->morphClass) diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index ad7d75168e78..d9a232931ae8 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(); @@ -260,7 +260,7 @@ public function getRelationExistenceCountQuery(Builder $query, Builder $parentQu * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 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/Events/ModelPruningStarting.php b/src/Illuminate/Database/Events/ModelPruningStarting.php index a45f912dc283..581d3da8a4bc 100644 --- a/src/Illuminate/Database/Events/ModelPruningStarting.php +++ b/src/Illuminate/Database/Events/ModelPruningStarting.php @@ -10,7 +10,7 @@ class ModelPruningStarting * @param array $models The class names of the models that will be pruned. */ public function __construct( - public $models + public $models, ) { } } diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index d56482dc889b..1d437f0566ce 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -31,8 +31,8 @@ public function __construct(Connection $connection) /** * Wrap an array of values. * - * @param array $values - * @return array + * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $values + * @return array */ public function wrapArray(array $values) { @@ -136,7 +136,7 @@ protected function wrapAliasedTable($value, $prefix = null) /** * Wrap the given value segments. * - * @param array $segments + * @param list $segments * @return string */ protected function wrapSegments($segments) @@ -190,7 +190,7 @@ protected function isJsonSelector($value) /** * Convert an array of column names into a delimited string. * - * @param array $columns + * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns * @return string */ public function columnize(array $columns) @@ -201,7 +201,7 @@ public function columnize(array $columns) /** * Create query parameter place-holders for an array. * - * @param array $values + * @param array $values * @return string */ public function parameterize(array $values) @@ -223,7 +223,7 @@ public function parameter($value) /** * Quote the given string literal. * - * @param string|array $value + * @param string|array $value * @return string */ public function quoteString($value) diff --git a/src/Illuminate/Database/LostConnectionDetector.php b/src/Illuminate/Database/LostConnectionDetector.php new file mode 100644 index 000000000000..921475a09a65 --- /dev/null +++ b/src/Illuminate/Database/LostConnectionDetector.php @@ -0,0 +1,94 @@ +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', + 'vtgate connection error: no healthy endpoints', + 'primary is not serving, there may be a reparent operation in progress', + 'current keyspace is being resharded', + 'no healthy tablet available', + 'transaction pool connection limit exceeded', + 'SSL operation failed with code 5', + ]); + } +} 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/MigrationCreator.php b/src/Illuminate/Database/Migrations/MigrationCreator.php index ba98eb658148..8f6b5bd45a48 100755 --- a/src/Illuminate/Database/Migrations/MigrationCreator.php +++ b/src/Illuminate/Database/Migrations/MigrationCreator.php @@ -82,7 +82,7 @@ public function create($name, $path, $table = null, $create = false) * Ensure that a migration with the given name doesn't already exist. * * @param string $name - * @param string $migrationPath + * @param string|null $migrationPath * @return void * * @throws \InvalidArgumentException 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 1f00b7bd655f..03195c5b9f4f 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -90,14 +90,17 @@ class Builder implements BuilderContract /** * An aggregate function and column to be run. * - * @var array|null + * @var array{ + * function: string, + * columns: array<\Illuminate\Contracts\Database\Query\Expression|string> + * }|null */ public $aggregate; /** * The columns that should be returned. * - * @var array|null + * @var array|null */ public $columns; @@ -275,7 +278,7 @@ public function __construct( /** * Set the columns to be selected. * - * @param array|mixed $columns + * @param mixed $columns * @return $this */ public function select($columns = ['*']) @@ -429,7 +432,7 @@ protected function prependDatabaseNameIfCrossDatabaseQuery($query) /** * Add a new select column to the query. * - * @param array|mixed $column + * @param mixed $column * @return $this */ public function addSelect($column) @@ -1098,7 +1101,7 @@ public function orWhereColumn($first, $operator = null, $second = null) /** * Add a raw where clause to the query. * - * @param string $sql + * @param \Illuminate\Contracts\Database\Query\Expression|string $sql * @param mixed $bindings * @param string $boolean * @return $this @@ -1486,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. * @@ -2853,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. * @@ -3016,7 +3087,7 @@ public function toRawSql() * Execute a query for a single record by ID. * * @param int|string $id - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @return object|null */ public function find($id, $columns = ['*']) @@ -3030,7 +3101,7 @@ public function find($id, $columns = ['*']) * @template TValue * * @param mixed $id - * @param (\Closure(): TValue)|list|string $columns + * @param (\Closure(): TValue)|string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param (\Closure(): TValue)|null $callback * @return object|TValue */ @@ -3059,7 +3130,7 @@ public function value($column) { $result = (array) $this->first([$column]); - return count($result) > 0 ? reset($result) : null; + return count($result) > 0 ? array_first($result) : null; } /** @@ -3071,7 +3142,7 @@ public function rawValue(string $expression, array $bindings = []) { $result = (array) $this->selectRaw($expression, $bindings)->first(); - return count($result) > 0 ? reset($result) : null; + return count($result) > 0 ? array_first($result) : null; } /** @@ -3087,13 +3158,13 @@ public function soleValue($column) { $result = (array) $this->sole([$column]); - return reset($result); + return array_first($result); } /** * Execute the query as a "select" statement. * - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @return \Illuminate\Support\Collection */ public function get($columns = ['*']) @@ -3149,7 +3220,7 @@ protected function withoutGroupLimitKeys($items) * Paginate the given query into a simple paginator. * * @param int|\Closure $perPage - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param string $pageName * @param int|null $page * @param \Closure|int|null $total @@ -3177,7 +3248,7 @@ public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $p * This is more efficient on larger data-sets, etc. * * @param int $perPage - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param string $pageName * @param int|null $page * @return \Illuminate\Contracts\Pagination\Paginator @@ -3200,7 +3271,7 @@ public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'pag * This is more efficient on larger data-sets, etc. * * @param int|null $perPage - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param string $cursorName * @param \Illuminate\Pagination\Cursor|string|null $cursor * @return \Illuminate\Contracts\Pagination\CursorPaginator @@ -3247,8 +3318,8 @@ protected function ensureOrderForCursorPagination($shouldReverse = false) /** * Get the count of the total records for the paginator. * - * @param array $columns - * @return int + * @param array $columns + * @return int<0, max> */ public function getCountForPagination($columns = ['*']) { @@ -3269,8 +3340,8 @@ public function getCountForPagination($columns = ['*']) /** * Run a pagination count query. * - * @param array $columns - * @return array + * @param array $columns + * @return array */ protected function runPaginationCountQuery($columns = ['*']) { @@ -3310,7 +3381,8 @@ protected function cloneForPaginationCount() /** * Remove the column aliases since they will break count queries. * - * @return array + * @param array $columns + * @return array */ protected function withoutSelectAliases(array $columns) { @@ -3536,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 = '*') { @@ -3653,7 +3725,7 @@ public function numericAggregate($function, $columns = ['*']) * Set the aggregate property without running the query. * * @param string $function - * @param array $columns + * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns * @return $this */ protected function setAggregate($function, $columns) @@ -3674,9 +3746,11 @@ protected function setAggregate($function, $columns) * * After running the callback, the columns are reset to the original value. * - * @param array $columns - * @param callable $callback - * @return mixed + * @template TResult + * + * @param array $columns + * @param callable(): TResult $callback + * @return TResult */ protected function onceWithColumns($columns, $callback) { @@ -3707,7 +3781,7 @@ public function insert(array $values) return true; } - if (! is_array(reset($values))) { + if (! is_array(array_first($values))) { $values = [$values]; } @@ -3736,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) { @@ -3744,7 +3818,7 @@ public function insertOrIgnore(array $values) return 0; } - if (! is_array(reset($values))) { + if (! is_array(array_first($values))) { $values = [$values]; } else { foreach ($values as $key => $value) { @@ -3818,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) { @@ -3892,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; @@ -3904,7 +3976,7 @@ public function upsert(array $values, $uniqueBy, $update = null) return (int) $this->insert($values); } - if (! is_array(reset($values))) { + if (! is_array(array_first($values))) { $values = [$values]; } else { foreach ($values as $key => $value) { @@ -3915,7 +3987,7 @@ public function upsert(array $values, $uniqueBy, $update = null) } if (is_null($update)) { - $update = array_keys(reset($values)); + $update = array_keys(array_first($values)); } $this->applyBeforeQueryCallbacks(); @@ -3938,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 */ @@ -3956,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 */ @@ -3980,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 */ @@ -3998,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 */ @@ -4078,7 +4150,7 @@ protected function forSubQuery() /** * Get all of the query builder's columns in a text-only array with all expressions evaluated. * - * @return array + * @return list */ public function getColumns() { @@ -4168,7 +4240,7 @@ public function getRawBindings() * Set the bindings on the query builder. * * @param list $bindings - * @param string $type + * @param "select"|"from"|"join"|"where"|"groupBy"|"having"|"order"|"union"|"unionOrder" $type * @return $this * * @throws \InvalidArgumentException @@ -4188,7 +4260,7 @@ public function setBindings(array $bindings, $type = 'where') * Add a binding to the query. * * @param mixed $value - * @param string $type + * @param "select"|"from"|"join"|"where"|"groupBy"|"having"|"order"|"union"|"unionOrder" $type * @return $this * * @throws \InvalidArgumentException diff --git a/src/Illuminate/Database/Query/Expression.php b/src/Illuminate/Database/Query/Expression.php index 1568e1ff9436..839dea833a2c 100755 --- a/src/Illuminate/Database/Query/Expression.php +++ b/src/Illuminate/Database/Query/Expression.php @@ -16,7 +16,7 @@ class Expression implements ExpressionContract * @param TValue $value */ public function __construct( - protected $value + protected $value, ) { } diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 9b35083af603..63faa6966bfe 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -123,7 +123,7 @@ protected function compileComponents(Builder $query) * Compile an aggregated select clause. * * @param \Illuminate\Database\Query\Builder $query - * @param array $aggregate + * @param array{function: string, columns: array<\Illuminate\Contracts\Database\Query\Expression|string>} $aggregate * @return string */ protected function compileAggregate(Builder $query, $aggregate) @@ -430,9 +430,9 @@ protected function whereBetween(Builder $query, $where) { $between = $where['not'] ? 'not between' : 'between'; - $min = $this->parameter(is_array($where['values']) ? reset($where['values']) : $where['values'][0]); + $min = $this->parameter(is_array($where['values']) ? array_first($where['values']) : $where['values'][0]); - $max = $this->parameter(is_array($where['values']) ? end($where['values']) : $where['values'][1]); + $max = $this->parameter(is_array($where['values']) ? array_last($where['values']) : $where['values'][1]); return $this->wrap($where['column']).' '.$between.' '.$min.' and '.$max; } @@ -448,13 +448,31 @@ protected function whereBetweenColumns(Builder $query, $where) { $between = $where['not'] ? 'not between' : 'between'; - $min = $this->wrap(is_array($where['values']) ? reset($where['values']) : $where['values'][0]); + $min = $this->wrap(is_array($where['values']) ? array_first($where['values']) : $where['values'][0]); - $max = $this->wrap(is_array($where['values']) ? end($where['values']) : $where['values'][1]); + $max = $this->wrap(is_array($where['values']) ? array_last($where['values']) : $where['values'][1]); 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']) ? array_first($where['columns']) : $where['columns'][0]); + + $max = $this->wrap(is_array($where['columns']) ? array_last($where['columns']) : $where['columns'][1]); + + return $this->parameter($where['value']).' '.$between.' '.$min.' and '.$max; + } + /** * Compile a "where date" clause. * @@ -969,7 +987,11 @@ protected function compileOrders(Builder $query, $orders) */ protected function compileOrdersToArray(Builder $query, $orders) { - return array_map(function ($order) { + return array_map(function ($order) use ($query) { + if (isset($order['sql']) && $order['sql'] instanceof Expression) { + return $order['sql']->getValue($query->getGrammar()); + } + return $order['sql'] ?? $this->wrap($order['column']).' '.$order['direction']; }, $orders); } @@ -1168,18 +1190,18 @@ public function compileInsert(Builder $query, array $values) return "insert into {$table} default values"; } - if (! is_array(reset($values))) { + if (! is_array(array_first($values))) { $values = [$values]; } - $columns = $this->columnize(array_keys(reset($values))); + $columns = $this->columnize(array_keys(array_first($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 +1298,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/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 4372bae32791..1856b998ca01 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -442,6 +442,7 @@ protected function compileUpdateWithoutJoins(Builder $query, $table, $columns, $ * @param array $values * @return array */ + #[\Override] public function prepareBindingsForUpdate(array $bindings, array $values) { $values = (new Collection($values)) diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index 9207fe54565f..3a2a7b425f3c 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'; @@ -618,6 +618,7 @@ protected function compileUpdateWithJoinsOrLimit(Builder $query, array $values) * @param array $values * @return array */ + #[\Override] public function prepareBindingsForUpdate(array $bindings, array $values) { $values = (new Collection($values))->map(function ($value, $column) { @@ -628,6 +629,8 @@ public function prepareBindingsForUpdate(array $bindings, array $values) $cleanBindings = Arr::except($bindings, 'select'); + $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); + return array_values( array_merge($values, Arr::flatten($cleanBindings)) ); diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 9fb8d8a31589..f9a82febd948 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(', '); } /** @@ -382,6 +384,7 @@ protected function compileUpdateWithJoinsOrLimit(Builder $query, array $values) * @param array $values * @return array */ + #[\Override] public function prepareBindingsForUpdate(array $bindings, array $values) { $groups = $this->groupJsonColumnsForUpdate($values); @@ -394,6 +397,8 @@ public function prepareBindingsForUpdate(array $bindings, array $values) $cleanBindings = Arr::except($bindings, 'select'); + $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); + return array_values( array_merge($values, Arr::flatten($cleanBindings)) ); diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index c5e91c50e1bf..2cd0d15a0be5 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -415,19 +415,19 @@ protected function compileUpdateWithJoins(Builder $query, $table, $columns, $whe */ public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update) { - $columns = $this->columnize(array_keys(reset($values))); + $columns = $this->columnize(array_keys(array_first($values))); $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.' '; @@ -453,10 +453,13 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar * @param array $values * @return array */ + #[\Override] public function prepareBindingsForUpdate(array $bindings, array $values) { $cleanBindings = Arr::except($bindings, 'select'); + $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); + return array_values( array_merge($values, Arr::flatten($cleanBindings)) ); 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/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index 871575a5c488..2818f91d8c1f 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -94,6 +94,7 @@ public function processColumns($results) 'generation' => $result->generated ? [ 'type' => match ($result->generated) { 's' => 'stored', + 'v' => 'virtual', default => null, }, 'expression' => $result->default, diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index 53cbfad42c9d..42a5947741f2 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -20,6 +20,24 @@ public function getDriverTitle() return 'SQLite'; } + /** + * Run the statement to start a new transaction. + * + * @return void + */ + protected function executeBeginTransactionStatement() + { + if (version_compare(PHP_VERSION, '8.4.0') >= 0) { + $mode = $this->getConfig('transaction_mode') ?? 'DEFERRED'; + + $this->getPdo()->exec("BEGIN {$mode} TRANSACTION"); + + return; + } + + $this->getPdo()->beginTransaction(); + } + /** * Escape a binary value for safe SQL embedding. * diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index b7687e839f34..de2233249055 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'); } /** @@ -407,7 +407,7 @@ public function dropIfExists() /** * Indicate that the given columns should be dropped. * - * @param array|mixed $columns + * @param mixed $columns * @return \Illuminate\Support\Fluent */ public function dropColumn($columns) @@ -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,52 @@ 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. + * Add nullable 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(); + return new Collection([ + $this->timestampTz('created_at', $precision)->nullable(), + $this->timestampTz('updated_at', $precision)->nullable(), + ]); + } - $this->timestampTz('updated_at', $precision)->nullable(); + /** + * Add nullable creation and update timestampTz columns to the table. + * + * Alias for self::timestampsTz(). + * + * @param int|null $precision + * @return \Illuminate\Support\Collection + */ + public function nullableTimestampsTz($precision = null) + { + return $this->timestampsTz($precision); } /** * 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(), + ]); } /** @@ -1479,16 +1498,17 @@ public function vector($column, $dimensions = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function morphs($name, $indexName = null) + public function morphs($name, $indexName = null, $after = null) { if (Builder::$defaultMorphKeyType === 'uuid') { - $this->uuidMorphs($name, $indexName); + $this->uuidMorphs($name, $indexName, $after); } elseif (Builder::$defaultMorphKeyType === 'ulid') { - $this->ulidMorphs($name, $indexName); + $this->ulidMorphs($name, $indexName, $after); } else { - $this->numericMorphs($name, $indexName); + $this->numericMorphs($name, $indexName, $after); } } @@ -1497,16 +1517,17 @@ public function morphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function nullableMorphs($name, $indexName = null) + public function nullableMorphs($name, $indexName = null, $after = null) { if (Builder::$defaultMorphKeyType === 'uuid') { - $this->nullableUuidMorphs($name, $indexName); + $this->nullableUuidMorphs($name, $indexName, $after); } elseif (Builder::$defaultMorphKeyType === 'ulid') { - $this->nullableUlidMorphs($name, $indexName); + $this->nullableUlidMorphs($name, $indexName, $after); } else { - $this->nullableNumericMorphs($name, $indexName); + $this->nullableNumericMorphs($name, $indexName, $after); } } @@ -1515,13 +1536,16 @@ public function nullableMorphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function numericMorphs($name, $indexName = null) + public function numericMorphs($name, $indexName = null, $after = null) { - $this->string("{$name}_type"); + $this->string("{$name}_type") + ->after($after); - $this->unsignedBigInteger("{$name}_id"); + $this->unsignedBigInteger("{$name}_id") + ->after(! is_null($after) ? "{$name}_type" : null); $this->index(["{$name}_type", "{$name}_id"], $indexName); } @@ -1531,13 +1555,18 @@ public function numericMorphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function nullableNumericMorphs($name, $indexName = null) + public function nullableNumericMorphs($name, $indexName = null, $after = null) { - $this->string("{$name}_type")->nullable(); + $this->string("{$name}_type") + ->nullable() + ->after($after); - $this->unsignedBigInteger("{$name}_id")->nullable(); + $this->unsignedBigInteger("{$name}_id") + ->nullable() + ->after(! is_null($after) ? "{$name}_type" : null); $this->index(["{$name}_type", "{$name}_id"], $indexName); } @@ -1547,13 +1576,16 @@ public function nullableNumericMorphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function uuidMorphs($name, $indexName = null) + public function uuidMorphs($name, $indexName = null, $after = null) { - $this->string("{$name}_type"); + $this->string("{$name}_type") + ->after($after); - $this->uuid("{$name}_id"); + $this->uuid("{$name}_id") + ->after(! is_null($after) ? "{$name}_type" : null); $this->index(["{$name}_type", "{$name}_id"], $indexName); } @@ -1563,13 +1595,18 @@ public function uuidMorphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function nullableUuidMorphs($name, $indexName = null) + public function nullableUuidMorphs($name, $indexName = null, $after = null) { - $this->string("{$name}_type")->nullable(); + $this->string("{$name}_type") + ->nullable() + ->after($after); - $this->uuid("{$name}_id")->nullable(); + $this->uuid("{$name}_id") + ->nullable() + ->after(! is_null($after) ? "{$name}_type" : null); $this->index(["{$name}_type", "{$name}_id"], $indexName); } @@ -1579,13 +1616,16 @@ public function nullableUuidMorphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function ulidMorphs($name, $indexName = null) + public function ulidMorphs($name, $indexName = null, $after = null) { - $this->string("{$name}_type"); + $this->string("{$name}_type") + ->after($after); - $this->ulid("{$name}_id"); + $this->ulid("{$name}_id") + ->after(! is_null($after) ? "{$name}_type" : null); $this->index(["{$name}_type", "{$name}_id"], $indexName); } @@ -1595,13 +1635,18 @@ public function ulidMorphs($name, $indexName = null) * * @param string $name * @param string|null $indexName + * @param string|null $after * @return void */ - public function nullableUlidMorphs($name, $indexName = null) + public function nullableUlidMorphs($name, $indexName = null, $after = null) { - $this->string("{$name}_type")->nullable(); + $this->string("{$name}_type") + ->nullable() + ->after($after); - $this->ulid("{$name}_id")->nullable(); + $this->ulid("{$name}_id") + ->nullable() + ->after(! is_null($after) ? "{$name}_type" : null); $this->index(["{$name}_type", "{$name}_id"], $indexName); } @@ -1640,15 +1685,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 +1704,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 109932a27d12..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 + * @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 $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/ForeignIdColumnDefinition.php b/src/Illuminate/Database/Schema/ForeignIdColumnDefinition.php index c7f66d19bb96..2501bebe68ab 100644 --- a/src/Illuminate/Database/Schema/ForeignIdColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ForeignIdColumnDefinition.php @@ -46,7 +46,7 @@ public function constrained($table = null, $column = null, $indexName = null) * Specify which column this foreign ID references on another table. * * @param string $column - * @param string $indexName + * @param string|null $indexName * @return \Illuminate\Database\Schema\ForeignKeyDefinition */ public function references($column, $indexName = null) diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index ed683d256d30..391324b9c6d2 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -2,13 +2,15 @@ namespace Illuminate\Database\Schema\Grammars; -use BackedEnum; use Illuminate\Contracts\Database\Query\Expression; use Illuminate\Database\Concerns\CompilesJsonPaths; use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Fluent; use RuntimeException; +use UnitEnum; + +use function Illuminate\Support\enum_value; abstract class Grammar extends BaseGrammar { @@ -387,7 +389,7 @@ protected function getCommandByName(Blueprint $blueprint, $name) $commands = $this->getCommandsByName($blueprint, $name); if (count($commands) > 0) { - return reset($commands); + return array_first($commands); } } @@ -477,13 +479,13 @@ protected function getDefaultValue($value) return $this->getValue($value); } - if ($value instanceof BackedEnum) { - return "'{$value->value}'"; + if ($value instanceof UnitEnum) { + return "'".str_replace("'", "''", enum_value($value))."'"; } return is_bool($value) ? "'".(int) $value."'" - : "'".(string) $value."'"; + : "'".str_replace("'", "''", $value)."'"; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index db992eac43ab..16e8634d3e6b 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -927,6 +927,16 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + $isMaria = $this->connection->isMaria(); + $version = $this->connection->getServerVersion(); + + if ($isMaria || + (! $isMaria && version_compare($version, '8.0.13', '>='))) { + if ($column->useCurrent) { + $column->default(new Expression('(CURDATE())')); + } + } + return 'date'; } @@ -1024,6 +1034,16 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + $isMaria = $this->connection->isMaria(); + $version = $this->connection->getServerVersion(); + + if ($isMaria || + (! $isMaria && version_compare($version, '8.0.13', '>='))) { + if ($column->useCurrent) { + $column->default(new Expression('(YEAR(CURDATE()))')); + } + } + return 'year'; } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index a40f7a62e153..73e78071a33e 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -323,7 +323,7 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command - * @return string + * @return string[] */ public function compileUnique(Blueprint $blueprint, Fluent $command) { @@ -333,12 +333,29 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) $uniqueStatement .= ' nulls '.($command->nullsNotDistinct ? 'not distinct' : 'distinct'); } - $sql = sprintf('alter table %s add constraint %s %s (%s)', - $this->wrapTable($blueprint), - $this->wrap($command->index), - $uniqueStatement, - $this->columnize($command->columns) - ); + if ($command->online || $command->algorithm) { + $createIndexSql = sprintf('create unique index %s%s on %s%s (%s)', + $command->online ? 'concurrently ' : '', + $this->wrap($command->index), + $this->wrapTable($blueprint), + $command->algorithm ? ' using '.$command->algorithm : '', + $this->columnize($command->columns) + ); + + $sql = sprintf('alter table %s add constraint %s unique using index %s', + $this->wrapTable($blueprint), + $this->wrap($command->index), + $this->wrap($command->index) + ); + } else { + $sql = sprintf( + 'alter table %s add constraint %s %s (%s)', + $this->wrapTable($blueprint), + $this->wrap($command->index), + $uniqueStatement, + $this->columnize($command->columns) + ); + } if (! is_null($command->deferrable)) { $sql .= $command->deferrable ? ' deferrable' : ' not deferrable'; @@ -348,7 +365,7 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) $sql .= $command->initiallyImmediate ? ' initially immediate' : ' initially deferred'; } - return $sql; + return isset($createIndexSql) ? [$createIndexSql, $sql] : [$sql]; } /** @@ -360,7 +377,8 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) */ public function compileIndex(Blueprint $blueprint, Fluent $command) { - return sprintf('create index %s on %s%s (%s)', + return sprintf('create index %s%s on %s%s (%s)', + $command->online ? 'concurrently ' : '', $this->wrap($command->index), $this->wrapTable($blueprint), $command->algorithm ? ' using '.$command->algorithm : '', @@ -385,7 +403,8 @@ public function compileFulltext(Blueprint $blueprint, Fluent $command) return "to_tsvector({$this->quoteString($language)}, {$this->wrap($column)})"; }, $command->columns); - return sprintf('create index %s on %s using gin ((%s))', + return sprintf('create index %s%s on %s using gin ((%s))', + $command->online ? 'concurrently ' : '', $this->wrap($command->index), $this->wrapTable($blueprint), implode(' || ', $columns) @@ -403,9 +422,47 @@ 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%s on %s%s (%s)', + $command->online ? 'concurrently ' : '', + $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. * @@ -922,6 +979,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CURRENT_DATE')); + } + return 'date'; } @@ -1007,6 +1068,10 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('EXTRACT(YEAR FROM CURRENT_DATE)')); + } + return $this->typeInteger($column); } @@ -1192,7 +1257,7 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column) } if (! is_null($column->virtualAs)) { - return " generated always as ({$this->getValue($column->virtualAs)})"; + return " generated always as ({$this->getValue($column->virtualAs)}) virtual"; } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 91222b7e83eb..8908836dd9c7 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -907,6 +907,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CURRENT_DATE')); + } + return 'date'; } @@ -992,6 +996,10 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression("(CAST(strftime('%Y', 'now') AS INTEGER))")); + } + return $this->typeInteger($column); } diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 5e183a5dce76..28b5e5a7a161 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -272,10 +272,11 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) */ public function compileUnique(Blueprint $blueprint, Fluent $command) { - return sprintf('create unique index %s on %s (%s)', + return sprintf('create unique index %s on %s (%s)%s', $this->wrap($command->index), $this->wrapTable($blueprint), - $this->columnize($command->columns) + $this->columnize($command->columns), + $command->online ? ' with (online = on)' : '' ); } @@ -288,10 +289,11 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) */ public function compileIndex(Blueprint $blueprint, Fluent $command) { - return sprintf('create index %s on %s (%s)', + return sprintf('create index %s on %s (%s)%s', $this->wrap($command->index), $this->wrapTable($blueprint), - $this->columnize($command->columns) + $this->columnize($command->columns), + $command->online ? ' with (online = on)' : '' ); } @@ -769,6 +771,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CAST(GETDATE() AS DATE)')); + } + return 'date'; } @@ -856,6 +862,10 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CAST(YEAR(GETDATE()) AS INTEGER)')); + } + return $this->typeInteger($column); } diff --git a/src/Illuminate/Database/Schema/IndexDefinition.php b/src/Illuminate/Database/Schema/IndexDefinition.php index d11a3c8daeed..96dba99ee566 100644 --- a/src/Illuminate/Database/Schema/IndexDefinition.php +++ b/src/Illuminate/Database/Schema/IndexDefinition.php @@ -10,6 +10,7 @@ * @method $this deferrable(bool $value = true) Specify that the unique index is deferrable (PostgreSQL) * @method $this initiallyImmediate(bool $value = true) Specify the default time to check the unique index constraint (PostgreSQL) * @method $this nullsNotDistinct(bool $value = true) Specify that the null values should not be treated as distinct (PostgreSQL) + * @method $this online(bool $value = true) Specify that index creation should not lock the table (PostgreSQL/SqlServer) */ class IndexDefinition extends Fluent { diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 30729f1ef2e8..427c943ff736 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -115,6 +115,11 @@ 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'; + // } + return $value; } diff --git a/src/Illuminate/Database/Schema/SqliteSchemaState.php b/src/Illuminate/Database/Schema/SqliteSchemaState.php index bda420fefe31..3d954a39de17 100644 --- a/src/Illuminate/Database/Schema/SqliteSchemaState.php +++ b/src/Illuminate/Database/Schema/SqliteSchemaState.php @@ -16,11 +16,11 @@ class SqliteSchemaState extends SchemaState */ public function dump(Connection $connection, $path) { - with($process = $this->makeProcess( - $this->baseCommand().' ".schema --indent"' - ))->setTimeout(null)->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ - // - ])); + $process = $this->makeProcess($this->baseCommand().' ".schema --indent"') + ->setTimeout(null) + ->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + // + ])); $migrations = preg_replace('/CREATE TABLE sqlite_.+?\);[\r\n]+/is', '', $process->getOutput()); @@ -39,9 +39,9 @@ public function dump(Connection $connection, $path) */ protected function appendMigrationData(string $path) { - with($process = $this->makeProcess( + $process = $this->makeProcess( $this->baseCommand().' ".dump \''.$this->getMigrationTable().'\'"' - ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + )->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ // ])); diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index 08e57b2d7834..cac50afb579d 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -50,10 +50,8 @@ public function call($class, $silent = false, array $parameters = []) $name = get_class($seeder); if ($silent === false && isset($this->command)) { - with(new TwoColumnDetail($this->command->getOutput()))->render( - $name, - 'RUNNING' - ); + (new TwoColumnDetail($this->command->getOutput())) + ->render($name, 'RUNNING'); } $startTime = microtime(true); @@ -63,10 +61,8 @@ public function call($class, $silent = false, array $parameters = []) if ($silent === false && isset($this->command)) { $runTime = number_format((microtime(true) - $startTime) * 1000); - with(new TwoColumnDetail($this->command->getOutput()))->render( - $name, - "$runTime ms DONE" - ); + (new TwoColumnDetail($this->command->getOutput())) + ->render($name, "$runTime ms DONE"); $this->command->getOutput()->writeln(''); } diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index 606c093f1ba9..4f2cdb7af457 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -17,13 +17,14 @@ "require": { "php": "^8.2", "ext-pdo": "*", - "brick/math": "^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "illuminate/collections": "^12.0", "illuminate/container": "^12.0", "illuminate/contracts": "^12.0", "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", - "laravel/serializable-closure": "^1.3|^2.0" + "laravel/serializable-closure": "^1.3|^2.0", + "symfony/polyfill-php85": "^1.33" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index ee409586efc8..9bb38a100def 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -21,6 +21,8 @@ use Illuminate\Support\Traits\ReflectsClosures; use ReflectionClass; +use function Illuminate\Support\enum_value; + class Dispatcher implements DispatcherContract { use Macroable, ReflectsClosures; @@ -67,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. * @@ -250,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. @@ -558,7 +587,7 @@ protected function createQueuedHandlerCallable($class, $method) /** * Determine if the given event handler should be dispatched after all database transactions have committed. * - * @param object|mixed $listener + * @param mixed $listener * @return bool */ protected function handlerShouldBeDispatchedAfterDatabaseTransactions($listener) @@ -631,8 +660,8 @@ protected function queueHandler($class, $method, $arguments) : $listener->delay ?? null; is_null($delay) - ? $connection->pushOn($queue, $job) - : $connection->laterOn($queue, $delay, $job); + ? $connection->pushOn(enum_value($queue), $job) + : $connection->laterOn(enum_value($queue), $delay, $job); } /** @@ -676,7 +705,12 @@ protected function propagateListenerOptions($listener, $job) $job->shouldBeEncrypted = $listener instanceof ShouldBeEncrypted; $job->timeout = $listener->timeout ?? null; $job->failOnTimeout = $listener->failOnTimeout ?? false; - $job->tries = $listener->tries ?? null; + $job->tries = method_exists($listener, 'tries') ? $listener->tries(...$data) : ($listener->tries ?? null); + $job->messageGroup = method_exists($listener, 'messageGroup') ? $listener->messageGroup(...$data) : ($listener->messageGroup ?? null); + $job->withDeduplicator(method_exists($listener, 'deduplicator') + ? $listener->deduplicator(...$data) + : (method_exists($listener, 'deduplicationId') ? $listener->deduplicationId(...) : null) + ); $job->through(array_merge( method_exists($listener, 'middleware') ? $listener->middleware(...$data) : [], @@ -766,6 +800,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/Events/QueuedClosure.php b/src/Illuminate/Events/QueuedClosure.php index a1a2d63d1fbb..54a853d0361c 100644 --- a/src/Illuminate/Events/QueuedClosure.php +++ b/src/Illuminate/Events/QueuedClosure.php @@ -31,6 +31,20 @@ class QueuedClosure */ public $queue; + /** + * The job "group" the job should be sent to. + * + * @var string|null + */ + public $messageGroup; + + /** + * The job deduplicator callback the job should use to generate the deduplication ID. + * + * @var \Laravel\SerializableClosure\SerializableClosure|null + */ + public $deduplicator; + /** * The number of seconds before the job should be made available. * @@ -81,6 +95,38 @@ public function onQueue($queue) return $this; } + /** + * Set the desired job "group". + * + * This feature is only supported by some queues, such as Amazon SQS. + * + * @param \UnitEnum|string $group + * @return $this + */ + public function onGroup($group) + { + $this->messageGroup = enum_value($group); + + return $this; + } + + /** + * Set the desired job deduplicator callback. + * + * This feature is only supported by some queues, such as Amazon SQS FIFO. + * + * @param callable|null $deduplicator + * @return $this + */ + public function withDeduplicator($deduplicator) + { + $this->deduplicator = $deduplicator instanceof Closure + ? new SerializableClosure($deduplicator) + : $deduplicator; + + return $this; + } + /** * Set the desired delay in seconds for the job. * @@ -121,7 +167,12 @@ public function resolve() 'catch' => (new Collection($this->catchCallbacks)) ->map(fn ($callback) => new SerializableClosure($callback)) ->all(), - ]))->onConnection($this->connection)->onQueue($this->queue)->delay($this->delay); + ])) + ->onConnection($this->connection) + ->onQueue($this->queue) + ->delay($this->delay) + ->onGroup($this->messageGroup) + ->withDeduplicator($this->deduplicator); }; } } diff --git a/src/Illuminate/Events/functions.php b/src/Illuminate/Events/functions.php index df1b0febf23d..36b778885c84 100644 --- a/src/Illuminate/Events/functions.php +++ b/src/Illuminate/Events/functions.php @@ -9,9 +9,8 @@ * Create a new queued Closure event listener. * * @param \Closure $closure - * @return \Illuminate\Events\QueuedClosure */ - function queueable(Closure $closure) + function queueable(Closure $closure): QueuedClosure { return new QueuedClosure($closure); } diff --git a/src/Illuminate/Filesystem/AwsS3V3Adapter.php b/src/Illuminate/Filesystem/AwsS3V3Adapter.php index 7b125e2a68fe..33b4bb9a1f3f 100644 --- a/src/Illuminate/Filesystem/AwsS3V3Adapter.php +++ b/src/Illuminate/Filesystem/AwsS3V3Adapter.php @@ -4,7 +4,7 @@ use Aws\S3\S3Client; use Illuminate\Support\Traits\Conditionable; -use League\Flysystem\AwsS3V3\AwsS3V3Adapter as S3Adapter; +use League\Flysystem\FilesystemAdapter as FlysystemAdapter; use League\Flysystem\FilesystemOperator; class AwsS3V3Adapter extends FilesystemAdapter @@ -22,11 +22,11 @@ class AwsS3V3Adapter extends FilesystemAdapter * Create a new AwsS3V3FilesystemAdapter instance. * * @param \League\Flysystem\FilesystemOperator $driver - * @param \League\Flysystem\AwsS3V3\AwsS3V3Adapter $adapter + * @param \League\Flysystem\FilesystemAdapter $adapter * @param array $config * @param \Aws\S3\S3Client $client */ - public function __construct(FilesystemOperator $driver, S3Adapter $adapter, array $config, S3Client $client) + public function __construct(FilesystemOperator $driver, FlysystemAdapter $adapter, array $config, S3Client $client) { parent::__construct($driver, $adapter, $config); 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..57056e670cb6 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); } @@ -311,6 +313,10 @@ function (&$parent) use ($config) { if (isset($config['visibility'])) { $parent['visibility'] = $config['visibility']; } + + if (isset($config['throw'])) { + $parent['throw'] = $config['throw']; + } } )); } @@ -324,7 +330,7 @@ function (&$parent) use ($config) { */ protected function createFlysystem(FlysystemAdapter $adapter, array $config) { - if ($config['read-only'] ?? false === true) { + if ($config['read-only'] ?? false) { $adapter = new ReadOnlyFilesystemAdapter($adapter); } diff --git a/src/Illuminate/Filesystem/functions.php b/src/Illuminate/Filesystem/functions.php index 761189a07cef..2db77650fd09 100644 --- a/src/Illuminate/Filesystem/functions.php +++ b/src/Illuminate/Filesystem/functions.php @@ -8,9 +8,8 @@ * * @param string|null $basePath * @param string ...$paths - * @return string */ - function join_paths($basePath, ...$paths) + function join_paths($basePath, ...$paths): string { foreach ($paths as $index => $path) { if (empty($path) && $path !== '0') { diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 9a18e7737177..7d30ed8fd0f0 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.11.0'; + const VERSION = '12.34.0'; /** * The base path for the Laravel installation. @@ -254,6 +254,7 @@ public static function inferBasePath() { return match (true) { isset($_ENV['APP_BASE_PATH']) => $_ENV['APP_BASE_PATH'], + isset($_SERVER['APP_BASE_PATH']) => $_SERVER['APP_BASE_PATH'], default => dirname(array_values(array_filter( array_keys(ClassLoader::getRegisteredLoaders()), fn ($path) => ! str_starts_with($path, 'phar://'), diff --git a/src/Illuminate/Foundation/Auth/Access/Authorizable.php b/src/Illuminate/Foundation/Auth/Access/Authorizable.php index d9a7d022cda9..8a90c793d667 100644 --- a/src/Illuminate/Foundation/Auth/Access/Authorizable.php +++ b/src/Illuminate/Foundation/Auth/Access/Authorizable.php @@ -10,7 +10,7 @@ trait Authorizable * Determine if the entity has the given abilities. * * @param iterable|\BackedEnum|string $abilities - * @param array|mixed $arguments + * @param mixed $arguments * @return bool */ public function can($abilities, $arguments = []) @@ -22,7 +22,7 @@ public function can($abilities, $arguments = []) * Determine if the entity has any of the given abilities. * * @param iterable|\BackedEnum|string $abilities - * @param array|mixed $arguments + * @param mixed $arguments * @return bool */ public function canAny($abilities, $arguments = []) @@ -34,7 +34,7 @@ public function canAny($abilities, $arguments = []) * Determine if the entity does not have the given abilities. * * @param iterable|\BackedEnum|string $abilities - * @param array|mixed $arguments + * @param mixed $arguments * @return bool */ public function cant($abilities, $arguments = []) @@ -46,7 +46,7 @@ public function cant($abilities, $arguments = []) * Determine if the entity does not have the given abilities. * * @param iterable|\BackedEnum|string $abilities - * @param array|mixed $arguments + * @param mixed $arguments * @return bool */ public function cannot($abilities, $arguments = []) diff --git a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php index adabdcec0576..4bff28944216 100644 --- a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php +++ b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php @@ -13,7 +13,7 @@ trait AuthorizesRequests * Authorize a given action for the current user. * * @param mixed $ability - * @param mixed|array $arguments + * @param mixed $arguments * @return \Illuminate\Auth\Access\Response * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -30,7 +30,7 @@ public function authorize($ability, $arguments = []) * * @param \Illuminate\Contracts\Auth\Authenticatable|mixed $user * @param mixed $ability - * @param mixed|array $arguments + * @param mixed $arguments * @return \Illuminate\Auth\Access\Response * * @throws \Illuminate\Auth\Access\AuthorizationException @@ -46,7 +46,7 @@ public function authorizeForUser($user, $ability, $arguments = []) * Guesses the ability's name if it wasn't provided. * * @param mixed $ability - * @param mixed|array $arguments + * @param mixed $arguments * @return array */ protected function parseAbilityAndArguments($ability, $arguments) @@ -108,7 +108,7 @@ public function authorizeResource($model, $parameter = null, array $options = [] /** * Get the map of resource methods to ability names. * - * @return array + * @return array */ protected function resourceAbilityMap() { @@ -126,7 +126,7 @@ protected function resourceAbilityMap() /** * Get the list of resource methods which do not have model parameters. * - * @return array + * @return list */ protected function resourceMethodsWithoutModels() { diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index 90bc446dca4e..a5588cf0694e 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -9,7 +9,9 @@ use Illuminate\Log\LogManager; use Illuminate\Support\Env; use Monolog\Handler\NullHandler; +use PHPUnit\Framework\TestCase; use PHPUnit\Runner\ErrorHandler; +use PHPUnit\Runner\Version; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\ErrorHandler\Error\FatalError; use Throwable; @@ -130,21 +132,21 @@ protected function shouldIgnoreDeprecationErrors() */ protected function ensureDeprecationLoggerIsConfigured() { - with(static::$app['config'], function ($config) { - if ($config->get('logging.channels.deprecations')) { - return; - } + $config = static::$app['config']; - $this->ensureNullLogDriverIsConfigured(); + if ($config->get('logging.channels.deprecations')) { + return; + } - if (is_array($options = $config->get('logging.deprecations'))) { - $driver = $options['channel'] ?? 'null'; - } else { - $driver = $options ?? 'null'; - } + $this->ensureNullLogDriverIsConfigured(); - $config->set('logging.channels.deprecations', $config->get("logging.channels.{$driver}")); - }); + if (is_array($options = $config->get('logging.deprecations'))) { + $driver = $options['channel'] ?? 'null'; + } else { + $driver = $options ?? 'null'; + } + + $config->set('logging.channels.deprecations', $config->get("logging.channels.{$driver}")); } /** @@ -154,16 +156,16 @@ protected function ensureDeprecationLoggerIsConfigured() */ protected function ensureNullLogDriverIsConfigured() { - with(static::$app['config'], function ($config) { - if ($config->get('logging.channels.null')) { - return; - } + $config = static::$app['config']; - $config->set('logging.channels.null', [ - 'driver' => 'monolog', - 'handler' => NullHandler::class, - ]); - }); + if ($config->get('logging.channels.null')) { + return; + } + + $config->set('logging.channels.null', [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ]); } /** @@ -304,15 +306,16 @@ public static function forgetApp() /** * Flush the bootstrapper's global state. * + * @param \PHPUnit\Framework\TestCase|null $testCase * @return void */ - public static function flushState() + public static function flushState(?TestCase $testCase = null) { if (is_null(static::$app)) { return; } - static::flushHandlersState(); + static::flushHandlersState($testCase); static::$app = null; @@ -322,31 +325,16 @@ public static function flushState() /** * Flush the bootstrapper's global handlers state. * + * @param \PHPUnit\Framework\TestCase|null $testCase * @return void */ - public static function flushHandlersState() + public static function flushHandlersState(?TestCase $testCase = null) { - while (true) { - $previousHandler = set_exception_handler(static fn () => null); - - restore_exception_handler(); - - if ($previousHandler === null) { - break; - } - + while (get_exception_handler() !== null) { restore_exception_handler(); } - while (true) { - $previousHandler = set_error_handler(static fn () => null); - - restore_error_handler(); - - if ($previousHandler === null) { - break; - } - + while (get_error_handler() !== null) { restore_error_handler(); } @@ -355,7 +343,12 @@ public static function flushHandlersState() if ((fn () => $this->enabled ?? false)->call($instance)) { $instance->disable(); - $instance->enable(); + + if (version_compare(Version::id(), '12.3.4', '>=')) { + $instance->enable($testCase); + } else { + $instance->enable(); + } } } } 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/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index 443eb5eddf5a..278050f2997d 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -63,6 +63,36 @@ public function onQueue($queue) return $this; } + /** + * Set the desired job "group". + * + * This feature is only supported by some queues, such as Amazon SQS. + * + * @param \UnitEnum|string $group + * @return $this + */ + public function onGroup($group) + { + $this->job->onGroup($group); + + return $this; + } + + /** + * Set the desired job deduplicator callback. + * + * This feature is only supported by some queues, such as Amazon SQS FIFO. + * + * @param callable|null $deduplicator + * @return $this + */ + public function withDeduplicator($deduplicator) + { + $this->job->withDeduplicator($deduplicator); + + return $this; + } + /** * Set the desired connection for the chain. * diff --git a/src/Illuminate/Foundation/Cloud.php b/src/Illuminate/Foundation/Cloud.php index 046025fabe22..63cfe86a07c8 100644 --- a/src/Illuminate/Foundation/Cloud.php +++ b/src/Illuminate/Foundation/Cloud.php @@ -123,6 +123,7 @@ public static function configureCloudLogging(Application $app): void $app['config']->set('logging.channels.laravel-cloud-socket', [ 'driver' => 'monolog', + 'level' => $_ENV['LOG_LEVEL'] ?? $_SERVER['LOG_LEVEL'] ?? 'debug', 'handler' => SocketHandler::class, 'formatter' => JsonFormatter::class, 'formatter_with' => [ diff --git a/src/Illuminate/Foundation/ComposerScripts.php b/src/Illuminate/Foundation/ComposerScripts.php index 8cf568409138..f1b7a8f10bd7 100644 --- a/src/Illuminate/Foundation/ComposerScripts.php +++ b/src/Illuminate/Foundation/ComposerScripts.php @@ -2,7 +2,10 @@ namespace Illuminate\Foundation; +use Composer\Installer\PackageEvent; use Composer\Script\Event; +use Illuminate\Container\Container; +use Illuminate\Contracts\Console\Kernel; class ComposerScripts { @@ -45,6 +48,38 @@ public static function postAutoloadDump(Event $event) static::clearCompiled(); } + /** + * Handle the pre-package-uninstall Composer event. + * + * @param \Composer\Installer\PackageEvent $event + * @return void + */ + public static function prePackageUninstall(PackageEvent $event) + { + $bootstrapFile = dirname($vendorDir = $event->getComposer()->getConfig()->get('vendor-dir')).'/bootstrap/app.php'; + + if (! file_exists($bootstrapFile)) { + return; + } + + require_once $vendorDir.'/autoload.php'; + + if (! defined('LARAVEL_START')) { + define('LARAVEL_START', microtime(true)); + } + + require_once $bootstrapFile; + + /** @var Application $app */ + $app = Container::getInstance(); + $app->make(Kernel::class)->bootstrap(); + + /** @var \Composer\DependencyResolver\Operation\UninstallOperation $uninstallOperation */ + $uninstallOperation = $event->getOperation()->getPackage(); + + $app['events']->dispatch('composer_package.'.$uninstallOperation->getName().':pre_uninstall'); + } + /** * Clear the cached Laravel bootstrapping files. * diff --git a/src/Illuminate/Foundation/Concerns/ResolvesDumpSource.php b/src/Illuminate/Foundation/Concerns/ResolvesDumpSource.php index 318ec6c57f3b..d8efa1fbc29a 100644 --- a/src/Illuminate/Foundation/Concerns/ResolvesDumpSource.php +++ b/src/Illuminate/Foundation/Concerns/ResolvesDumpSource.php @@ -15,19 +15,25 @@ trait ResolvesDumpSource 'atom' => 'atom://core/open/file?filename={file}&line={line}', 'cursor' => 'cursor://file/{file}:{line}', 'emacs' => 'emacs://open?url=file://{file}&line={line}', + 'fleet' => 'fleet://open?file={file}&line={line}', 'idea' => 'idea://open?file={file}&line={line}', + 'kiro' => 'kiro://file/{file}:{line}', 'macvim' => 'mvim://open/?url=file://{file}&line={line}', + 'neovim' => 'nvim://open?url=file://{file}&line={line}', 'netbeans' => 'netbeans://open/?f={file}:{line}', 'nova' => 'nova://core/open/file?filename={file}&line={line}', 'phpstorm' => 'phpstorm://open?file={file}&line={line}', 'sublime' => 'subl://open?url=file://{file}&line={line}', 'textmate' => 'txmt://open?url=file://{file}&line={line}', + 'trae' => 'trae://file/{file}:{line}', 'vscode' => 'vscode://file/{file}:{line}', 'vscode-insiders' => 'vscode-insiders://file/{file}:{line}', 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/{file}:{line}', 'vscode-remote' => 'vscode://vscode-remote/{file}:{line}', 'vscodium' => 'vscodium://file/{file}:{line}', + 'windsurf' => 'windsurf://file/{file}:{line}', 'xdebug' => 'xdebug://{file}@{line}', + 'zed' => 'zed://file/{file}:{line}', ]; /** diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index f8506bc07dd4..e386d8273c8e 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -92,12 +92,12 @@ public function withProviders(array $providers = [], bool $withBootstrapProvider /** * Register the core event service provider for the application. * - * @param array|bool $discover + * @param iterable|bool $discover * @return $this */ - public function withEvents(array|bool $discover = []) + public function withEvents(iterable|bool $discover = true) { - if (is_array($discover) && count($discover) > 0) { + if (is_iterable($discover)) { AppEventServiceProvider::setEventDiscoveryPaths($discover); } @@ -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/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index 20b50499aedf..a458bf88ef47 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -555,8 +555,8 @@ public function redirectUsersTo(callable|string $redirect) /** * Configure where users are redirected by the authentication and guest middleware. * - * @param callable|string $guests - * @param callable|string $users + * @param callable|string|null $guests + * @param callable|string|null $users * @return $this */ public function redirectTo(callable|string|null $guests = null, callable|string|null $users = null) diff --git a/src/Illuminate/Foundation/Console/AboutCommand.php b/src/Illuminate/Foundation/Console/AboutCommand.php index c6d61b6d8303..8df4e28e1861 100644 --- a/src/Illuminate/Foundation/Console/AboutCommand.php +++ b/src/Illuminate/Foundation/Console/AboutCommand.php @@ -183,7 +183,7 @@ protected function gatherApplicationInformation() 'Config' => static::format($this->laravel->configurationIsCached(), console: $formatCachedStatus), 'Events' => static::format($this->laravel->eventsAreCached(), console: $formatCachedStatus), 'Routes' => static::format($this->laravel->routesAreCached(), console: $formatCachedStatus), - 'Views' => static::format($this->hasPhpFiles($this->laravel->storagePath('framework/views')), console: $formatCachedStatus), + 'Views' => static::format($this->hasPhpFiles(config('view.compiled')), console: $formatCachedStatus), ]); static::addToSection('Drivers', fn () => array_filter([ diff --git a/src/Illuminate/Foundation/Console/ApiInstallCommand.php b/src/Illuminate/Foundation/Console/ApiInstallCommand.php index 9c460555e80a..f8e19c4853f8 100644 --- a/src/Illuminate/Foundation/Console/ApiInstallCommand.php +++ b/src/Illuminate/Foundation/Console/ApiInstallCommand.php @@ -37,7 +37,7 @@ class ApiInstallCommand extends Command /** * Execute the console command. * - * @return int + * @return void */ public function handle() { @@ -67,12 +67,11 @@ public function handle() } if ($this->option('passport')) { - Process::run(array_filter([ + Process::run([ php_binary(), artisan_binary(), 'passport:install', - $this->confirm('Would you like to use UUIDs for all client IDs?') ? '--uuids' : null, - ])); + ]); $this->components->info('API scaffolding installed. Please add the [Laravel\Passport\HasApiTokens] trait to your User model.'); } else { @@ -150,7 +149,7 @@ protected function installSanctum() protected function installPassport() { $this->requireComposerPackages($this->option('composer'), [ - 'laravel/passport:^12.0', + 'laravel/passport:^13.0', ]); } } diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index b23689671d18..028825255c4d 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -5,12 +5,16 @@ use Composer\InstalledVersions; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Env; use Illuminate\Support\Facades\Process; use Symfony\Component\Console\Attribute\AsCommand; use function Illuminate\Support\artisan_binary; use function Illuminate\Support\php_binary; use function Laravel\Prompts\confirm; +use function Laravel\Prompts\password; +use function Laravel\Prompts\select; +use function Laravel\Prompts\text; #[AsCommand(name: 'install:broadcasting')] class BroadcastingInstallCommand extends Command @@ -26,6 +30,9 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} + {--reverb : Install Laravel Reverb as the default broadcaster} + {--pusher : Install Pusher as the default broadcaster} + {--ably : Install Ably as the default broadcaster} {--without-node : Do not prompt to install Node dependencies}'; /** @@ -35,10 +42,27 @@ class BroadcastingInstallCommand extends Command */ protected $description = 'Create a broadcasting channel routes file'; + /** + * The broadcasting driver to use. + * + * @var string|null + */ + protected $driver = null; + + /** + * The framework packages to install. + * + * @var array + */ + protected $frameworkPackages = [ + 'react' => '@laravel/echo-react', + 'vue' => '@laravel/echo-vue', + ]; + /** * Execute the console command. * - * @return int + * @return void */ public function handle() { @@ -54,25 +78,56 @@ public function handle() $this->uncommentChannelsRoutesFile(); $this->enableBroadcastServiceProvider(); - // Install bootstrapping... - if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { - if (! is_dir($directory = $this->laravel->resourcePath('js'))) { - mkdir($directory, 0755, true); - } + $this->driver = $this->resolveDriver(); - copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); - } + Env::writeVariable('BROADCAST_CONNECTION', $this->driver, $this->laravel->basePath('.env'), true); - if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { - $bootstrapScript = file_get_contents( - $bootstrapScriptPath - ); + $this->collectDriverConfig(); + $this->installDriverPackages(); + + if ($this->isUsingSupportedFramework()) { + // If this is a supported framework, we will use the framework-specific Echo helpers... + $this->injectFrameworkSpecificConfiguration(); + } else { + // Standard JavaScript implementation... + if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { + if (! is_dir($directory = $this->laravel->resourcePath('js'))) { + mkdir($directory, 0755, true); + } + + $stubPath = __DIR__.'/stubs/echo-js-'.$this->driver.'.stub'; + + if (! file_exists($stubPath)) { + $stubPath = __DIR__.'/stubs/echo-js-reverb.stub'; + } + + copy($stubPath, $echoScriptPath); + } - if (! str_contains($bootstrapScript, './echo')) { - file_put_contents( - $bootstrapScriptPath, - trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + // Only add the bootstrap import for the standard JS implementation... + if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { + $bootstrapScript = file_get_contents( + $bootstrapScriptPath ); + + if (! str_contains($bootstrapScript, './echo')) { + file_put_contents( + $bootstrapScriptPath, + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } + } elseif (file_exists($appScriptPath = $this->laravel->resourcePath('js/app.js'))) { + // If no bootstrap.js, try app.js... + $appScript = file_get_contents( + $appScriptPath + ); + + if (! str_contains($appScript, './echo')) { + file_put_contents( + $appScriptPath, + trim($appScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } } } @@ -118,8 +173,10 @@ protected function enableBroadcastServiceProvider() { $filesystem = new Filesystem; - if (! $filesystem->exists(app()->configPath('app.php')) || - ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php')) { + if ( + ! $filesystem->exists(app()->configPath('app.php')) || + ! $filesystem->exists('app/Providers/BroadcastServiceProvider.php') + ) { return; } @@ -134,6 +191,171 @@ protected function enableBroadcastServiceProvider() } } + /** + * Collect the driver configuration. + * + * @return void + */ + protected function collectDriverConfig() + { + $envPath = $this->laravel->basePath('.env'); + + if (! file_exists($envPath)) { + return; + } + + match ($this->driver) { + 'pusher' => $this->collectPusherConfig(), + 'ably' => $this->collectAblyConfig(), + default => null, + }; + } + + /** + * Install the driver packages. + * + * @return void + */ + protected function installDriverPackages() + { + $package = match ($this->driver) { + 'pusher' => 'pusher/pusher-php-server', + 'ably' => 'ably/ably-php', + default => null, + }; + + if (! $package || InstalledVersions::isInstalled($package)) { + return; + } + + $this->requireComposerPackages($this->option('composer'), [$package]); + } + + /** + * Collect the Pusher configuration. + * + * @return void + */ + protected function collectPusherConfig() + { + $appId = text('Pusher App ID', 'Enter your Pusher app ID'); + $key = password('Pusher App Key', 'Enter your Pusher app key'); + $secret = password('Pusher App Secret', 'Enter your Pusher app secret'); + + $cluster = select('Pusher App Cluster', [ + 'mt1', + 'us2', + 'us3', + 'eu', + 'ap1', + 'ap2', + 'ap3', + 'ap4', + 'sa1', + ]); + + Env::writeVariables([ + 'PUSHER_APP_ID' => $appId, + 'PUSHER_APP_KEY' => $key, + 'PUSHER_APP_SECRET' => $secret, + 'PUSHER_APP_CLUSTER' => $cluster, + 'PUSHER_PORT' => 443, + 'PUSHER_SCHEME' => 'https', + 'VITE_PUSHER_APP_KEY' => '${PUSHER_APP_KEY}', + 'VITE_PUSHER_APP_CLUSTER' => '${PUSHER_APP_CLUSTER}', + 'VITE_PUSHER_HOST' => '${PUSHER_HOST}', + 'VITE_PUSHER_PORT' => '${PUSHER_PORT}', + 'VITE_PUSHER_SCHEME' => '${PUSHER_SCHEME}', + ], $this->laravel->basePath('.env')); + } + + /** + * Collect the Ably configuration. + * + * @return void + */ + protected function collectAblyConfig() + { + $this->components->warn('Make sure to enable "Pusher protocol support" in your Ably app settings.'); + + $key = password('Ably Key', 'Enter your Ably key'); + + $publicKey = explode(':', $key)[0] ?? $key; + + Env::writeVariables([ + 'ABLY_KEY' => $key, + 'ABLY_PUBLIC_KEY' => $publicKey, + 'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}', + ], $this->laravel->basePath('.env')); + } + + /** + * Inject Echo configuration into the application's main file. + * + * @return void + */ + protected function injectFrameworkSpecificConfiguration() + { + if ($this->appUsesVue()) { + $importPath = $this->frameworkPackages['vue']; + + $filePaths = [ + $this->laravel->resourcePath('js/app.ts'), + $this->laravel->resourcePath('js/app.js'), + ]; + } else { + $importPath = $this->frameworkPackages['react']; + + $filePaths = [ + $this->laravel->resourcePath('js/app.tsx'), + $this->laravel->resourcePath('js/app.jsx'), + ]; + } + + $filePath = array_filter($filePaths, function ($path) { + return file_exists($path); + })[0] ?? null; + + if (! $filePath) { + $this->components->warn("Could not find file [{$filePaths[0]}]. Skipping automatic Echo configuration."); + + return; + } + + $contents = file_get_contents($filePath); + + $echoCode = <<driver}', + }); + JS; + + preg_match_all('/^import .+;$/m', $contents, $matches); + + if (empty($matches[0])) { + // Add the Echo configuration to the top of the file if no import statements are found... + $newContents = $echoCode.PHP_EOL.$contents; + + file_put_contents($filePath, $newContents); + } else { + // Add Echo configuration after the last import... + $lastImport = array_last($matches[0]); + + $positionOfLastImport = strrpos($contents, $lastImport); + + if ($positionOfLastImport !== false) { + $insertPosition = $positionOfLastImport + strlen($lastImport); + $newContents = substr($contents, 0, $insertPosition).PHP_EOL.$echoCode.substr($contents, $insertPosition); + + file_put_contents($filePath, $newContents); + } + } + + $this->components->info('Echo configuration added to ['.basename($filePath).'].'); + } + /** * Install Laravel Reverb into the application if desired. * @@ -141,13 +363,11 @@ protected function enableBroadcastServiceProvider() */ protected function installReverb() { - if ($this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { + if ($this->driver !== 'reverb' || $this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { return; } - $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; } @@ -199,6 +419,12 @@ protected function installNodeDependencies() ]; } + if ($this->appUsesVue()) { + $commands[0] .= ' '.$this->frameworkPackages['vue']; + } elseif ($this->appUsesReact()) { + $commands[0] .= ' '.$this->frameworkPackages['react']; + } + $command = Process::command(implode(' && ', $commands)) ->path(base_path()); @@ -212,4 +438,79 @@ protected function installNodeDependencies() $this->components->info('Node dependencies installed successfully.'); } } + + /** + * Resolve the provider to use based on the user's choice. + * + * @return string + */ + protected function resolveDriver(): string + { + if ($this->option('reverb')) { + return 'reverb'; + } + + if ($this->option('pusher')) { + return 'pusher'; + } + + if ($this->option('ably')) { + return 'ably'; + } + + return select('Which broadcasting driver would you like to use?', [ + 'reverb' => 'Laravel Reverb', + 'pusher' => 'Pusher', + 'ably' => 'Ably', + ]); + } + + /** + * Detect if the user is using a supported framework (React or Vue). + * + * @return bool + */ + protected function isUsingSupportedFramework(): bool + { + return $this->appUsesReact() || $this->appUsesVue(); + } + + /** + * Detect if the user is using React. + * + * @return bool + */ + protected function appUsesReact(): bool + { + return $this->packageDependenciesInclude('react'); + } + + /** + * Detect if the user is using Vue. + * + * @return bool + */ + protected function appUsesVue(): bool + { + return $this->packageDependenciesInclude('vue'); + } + + /** + * Detect if the package is installed. + * + * @return bool + */ + protected function packageDependenciesInclude(string $package): bool + { + $packageJsonPath = $this->laravel->basePath('package.json'); + + if (! file_exists($packageJsonPath)) { + return false; + } + + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + + return isset($packageJson['dependencies'][$package]) || + isset($packageJson['devDependencies'][$package]); + } } diff --git a/src/Illuminate/Foundation/Console/ClosureCommand.php b/src/Illuminate/Foundation/Console/ClosureCommand.php index a9817d4df22d..f781ba44d2ea 100644 --- a/src/Illuminate/Foundation/Console/ClosureCommand.php +++ b/src/Illuminate/Foundation/Console/ClosureCommand.php @@ -25,6 +25,13 @@ class ClosureCommand extends Command */ protected $callback; + /** + * The console command description. + * + * @var string + */ + protected $description = ''; + /** * Create a new command instance. * diff --git a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php index 221ef95caecb..ec4b5954286f 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')) { @@ -63,8 +63,14 @@ public function handle() */ protected function writeView() { + $separator = '/'; + + if (windows_os()) { + $separator = '\\'; + } + $path = $this->viewPath( - str_replace('.', '/', $this->getView()).'.blade.php' + str_replace('.', $separator, $this->getView()).'.blade.php' ); if (! $this->files->isDirectory(dirname($path))) { diff --git a/src/Illuminate/Foundation/Console/ConfigCacheCommand.php b/src/Illuminate/Foundation/Console/ConfigCacheCommand.php index 6cef3389ea64..cedf0309dabc 100644 --- a/src/Illuminate/Foundation/Console/ConfigCacheCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigCacheCommand.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Arr; use LogicException; use Symfony\Component\Console\Attribute\AsCommand; use Throwable; @@ -69,6 +70,14 @@ public function handle() } catch (Throwable $e) { $this->files->delete($configPath); + foreach (Arr::dot($config) as $key => $value) { + try { + eval(var_export($value, true).';'); + } catch (Throwable $e) { + throw new LogicException("Your configuration files could not be serialized because the value at \"{$key}\" is non-serializable.", 0, $e); + } + } + throw new LogicException('Your configuration files are not serializable.', 0, $e); } diff --git a/src/Illuminate/Foundation/Console/ConfigMakeCommand.php b/src/Illuminate/Foundation/Console/ConfigMakeCommand.php new file mode 100644 index 000000000000..c4996d724c95 --- /dev/null +++ b/src/Illuminate/Foundation/Console/ConfigMakeCommand.php @@ -0,0 +1,86 @@ + + */ + protected $aliases = ['config:make']; + + /** + * Get the destination file path. + * + * @param string $name + */ + protected function getPath($name): string + { + return config_path(Str::finish($this->argument('name'), '.php')); + } + + /** + * Get the stub file for the generator. + */ + protected function getStub(): string + { + $relativePath = join_paths('stubs', 'config.stub'); + + return file_exists($customPath = $this->laravel->basePath($relativePath)) + ? $customPath + : join_paths(__DIR__, $relativePath); + } + + /** + * Get the console command arguments. + */ + protected function getOptions(): array + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the configuration file even if it already exists'], + ]; + } + + /** + * Prompt for missing input arguments using the returned questions. + * + * @return array + */ + protected function promptForMissingArgumentsUsing() + { + return [ + 'name' => 'What should the configuration file be named?', + ]; + } +} 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/DocsCommand.php b/src/Illuminate/Foundation/Console/DocsCommand.php index 1eea49070cc5..9edca71de61c 100644 --- a/src/Illuminate/Foundation/Console/DocsCommand.php +++ b/src/Illuminate/Foundation/Console/DocsCommand.php @@ -125,11 +125,11 @@ public function handle(Http $http, Cache $cache) */ protected function openUrl() { - with($this->url(), function ($url) { - $this->components->info("Opening the docs to: {$url}"); + $url = $this->url(); - $this->open($url); - }); + $this->components->info("Opening the docs to: {$url}"); + + $this->open($url); } /** @@ -145,9 +145,9 @@ protected function url() ]); } - return with($this->page(), function ($page) { - return trim("https://laravel.com/docs/{$this->version()}/{$page}#{$this->section($page)}", '#/'); - }); + $page = $this->page(); + + return trim("https://laravel.com/docs/{$this->version()}/{$page}#{$this->section($page)}", '#/'); } /** @@ -157,15 +157,15 @@ protected function url() */ protected function page() { - return with($this->resolvePage(), function ($page) { - if ($page === null) { - $this->components->warn('Unable to determine the page you are trying to visit.'); + $page = $this->resolvePage(); - return '/'; - } + if ($page === null) { + $this->components->warn('Unable to determine the page you are trying to visit.'); - return $page; - }); + return '/'; + } + + return $page; } /** @@ -441,11 +441,11 @@ public function docs() */ protected function refreshDocs() { - with($this->fetchDocs(), function ($response) { - if ($response->successful()) { - $this->cache->put("artisan.docs.{{$this->version()}}.index", $response->collect(), CarbonInterval::months(2)); - } - }); + $response = $this->fetchDocs(); + + if ($response->successful()) { + $this->cache->put("artisan.docs.{{$this->version()}}.index", $response->collect(), CarbonInterval::months(2)); + } } /** 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/MailMakeCommand.php b/src/Illuminate/Foundation/Console/MailMakeCommand.php index daa46d9e0abc..cc5bb3375873 100644 --- a/src/Illuminate/Foundation/Console/MailMakeCommand.php +++ b/src/Illuminate/Foundation/Console/MailMakeCommand.php @@ -67,8 +67,14 @@ public function handle() */ protected function writeMarkdownTemplate() { + $separator = '/'; + + if (windows_os()) { + $separator = '\\'; + } + $path = $this->viewPath( - str_replace('.', '/', $this->getView()).'.blade.php' + str_replace('.', $separator, $this->getView()).'.blade.php' ); if ($this->files->exists($path)) { @@ -89,8 +95,14 @@ protected function writeMarkdownTemplate() */ protected function writeView() { + $separator = '/'; + + if (windows_os()) { + $separator = '\\'; + } + $path = $this->viewPath( - str_replace('.', '/', $this->getView()).'.blade.php' + str_replace('.', $separator, $this->getView()).'.blade.php' ); if ($this->files->exists($path)) { 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/NotificationMakeCommand.php b/src/Illuminate/Foundation/Console/NotificationMakeCommand.php index 3156131b7a13..44d16438adc4 100644 --- a/src/Illuminate/Foundation/Console/NotificationMakeCommand.php +++ b/src/Illuminate/Foundation/Console/NotificationMakeCommand.php @@ -63,8 +63,14 @@ public function handle() */ protected function writeMarkdownTemplate() { + $separator = '/'; + + if (windows_os()) { + $separator = '\\'; + } + $path = $this->viewPath( - str_replace('.', '/', $this->option('markdown')).'.blade.php' + str_replace('.', $separator, $this->option('markdown')).'.blade.php' ); if (! $this->files->isDirectory(dirname($path))) { 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/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index e722046dd2ed..dc618f61a199 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -101,7 +101,13 @@ protected function initialize(InputInterface $input, OutputInterface $output) return false; } - return $workers > 1 && ! $this->option('no-reload') ? false : $workers; + if ($workers > 1 && + ! $this->option('no-reload') && + ! (int) env('LARAVEL_SAIL', 0)) { + return false; + } + + return $workers; }); parent::initialize($input, $output); diff --git a/src/Illuminate/Foundation/Console/stubs/config.stub b/src/Illuminate/Foundation/Console/stubs/config.stub new file mode 100644 index 000000000000..3ac44ad10138 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/config.stub @@ -0,0 +1,5 @@ +batch()->cancelled()) { + // The batch has been cancelled... + + return; + } + + // + } +} diff --git a/src/Illuminate/Foundation/Events/DiscoverEvents.php b/src/Illuminate/Foundation/Events/DiscoverEvents.php index e2933f937872..35f244837bce 100644 --- a/src/Illuminate/Foundation/Events/DiscoverEvents.php +++ b/src/Illuminate/Foundation/Events/DiscoverEvents.php @@ -2,6 +2,7 @@ namespace Illuminate\Foundation\Events; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Reflector; use Illuminate\Support\Str; @@ -16,19 +17,23 @@ class DiscoverEvents /** * The callback to be used to guess class names. * - * @var callable(SplFileInfo, string): string|null + * @var (callable(SplFileInfo, string): class-string)|null */ public static $guessClassNamesUsingCallback; /** * Get all of the events and listeners by searching the given listener directory. * - * @param string $listenerPath + * @param array|string $listenerPath * @param string $basePath * @return array */ public static function within($listenerPath, $basePath) { + if (Arr::wrap($listenerPath) === []) { + return []; + } + $listeners = new Collection(static::getListenerEvents( Finder::create()->files()->in($listenerPath), $basePath )); @@ -51,7 +56,7 @@ public static function within($listenerPath, $basePath) /** * Get all of the listeners and their corresponding events. * - * @param iterable $listeners + * @param iterable $listeners * @param string $basePath * @return array */ @@ -91,7 +96,7 @@ protected static function getListenerEvents($listeners, $basePath) * * @param \SplFileInfo $file * @param string $basePath - * @return string + * @return class-string */ protected static function classFromFile(SplFileInfo $file, $basePath) { @@ -111,7 +116,7 @@ protected static function classFromFile(SplFileInfo $file, $basePath) /** * Specify a callback to be used to guess class names. * - * @param callable(SplFileInfo, string): string $callback + * @param callable(SplFileInfo, string): class-string $callback * @return void */ public static function guessClassNamesUsing(callable $callback) diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 00a69266dcc7..57dfc30f480e 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -22,7 +22,6 @@ use Illuminate\Database\RecordsNotFoundException; use Illuminate\Foundation\Exceptions\Renderer\Renderer; use Illuminate\Http\Exceptions\HttpResponseException; -use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Response; use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException; @@ -72,6 +71,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 +285,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 +436,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; @@ -955,7 +984,7 @@ protected function toIlluminateResponse($response, Throwable $e) $response->getTargetUrl(), $response->getStatusCode(), $response->headers->all() ); } else { - $response = new Response( + $response = response( $response->getContent(), $response->getStatusCode(), $response->headers->all() ); } @@ -972,7 +1001,7 @@ protected function toIlluminateResponse($response, Throwable $e) */ protected function prepareJsonResponse($request, Throwable $e) { - return new JsonResponse( + return response()->json( $this->convertExceptionToArray($e), $this->isHttpException($e) ? $e->getStatusCode() : 500, $this->isHttpException($e) ? $e->getHeaders() : [], @@ -1016,12 +1045,12 @@ public function renderForConsole($output, Throwable $e) if (! empty($alternatives = $e->getAlternatives())) { $message .= '. Did you mean one of these?'; - with(new Error($output))->render($message); - with(new BulletList($output))->render($alternatives); + (new Error($output))->render($message); + (new BulletList($output))->render($alternatives); $output->writeln(''); } else { - with(new Error($output))->render($message); + (new Error($output))->render($message); } return; 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/Exceptions/Renderer/Exception.php b/src/Illuminate/Foundation/Exceptions/Renderer/Exception.php index eb65929f3150..59e9a94bd5f6 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Exception.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Exception.php @@ -87,17 +87,23 @@ public function class() } /** - * Get the first "non-vendor" frame index. + * Get the exception code. * - * @return int + * @return int|string */ - public function defaultFrame() + public function code() { - $key = array_search(false, array_map(function (Frame $frame) { - return $frame->isFromVendor(); - }, $this->frames()->all())); + return $this->exception->getCode(); + } - return $key === false ? 0 : $key; + /** + * Get the HTTP status code. + * + * @return int + */ + public function httpStatusCode() + { + return $this->exception->getStatusCode(); } /** @@ -107,22 +113,65 @@ public function defaultFrame() */ public function frames() { - $classMap = once(fn () => array_map(function ($path) { - return (string) realpath($path); - }, array_values(ClassLoader::getRegisteredLoaders())[0]->getClassMap())); + return once(function () { + $classMap = array_map(function ($path) { + return (string) realpath($path); + }, array_values(ClassLoader::getRegisteredLoaders())[0]->getClassMap()); + + $trace = array_values(array_filter( + $this->exception->getTrace(), fn ($trace) => isset($trace['file']), + )); + + if (($trace[1]['class'] ?? '') === HandleExceptions::class) { + array_shift($trace); + array_shift($trace); + } + + $frames = []; + $previousFrame = null; + + foreach (array_reverse($trace) as $frameData) { + $frame = new Frame($this->exception, $classMap, $frameData, $this->basePath, $previousFrame); + $frames[] = $frame; + $previousFrame = $frame; + } - $trace = array_values(array_filter( - $this->exception->getTrace(), fn ($trace) => isset($trace['file']), - )); + $frames = array_reverse($frames); + + foreach ($frames as $frame) { + if (! $frame->isFromVendor()) { + $frame->markAsMain(); + break; + } + } + + return new Collection($frames); + }); + } + + /** + * Get the exception's frames grouped by vendor status. + * + * @return array}> + */ + public function frameGroups() + { + $groups = []; + + foreach ($this->frames() as $frame) { + $isVendor = $frame->isFromVendor(); + + if (empty($groups) || $groups[array_key_last($groups)]['is_vendor'] !== $isVendor) { + $groups[] = [ + 'is_vendor' => $isVendor, + 'frames' => [], + ]; + } - if (($trace[1]['class'] ?? '') === HandleExceptions::class) { - array_shift($trace); - array_shift($trace); + $groups[array_key_last($groups)]['frames'][] = $frame; } - return new Collection(array_map( - fn (array $trace) => new Frame($this->exception, $classMap, $trace, $this->basePath), $trace, - )); + return $groups; } /** diff --git a/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php b/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php index efd6d7a62f63..c785dcd9d6e6 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Frame.php @@ -5,6 +5,8 @@ use Illuminate\Foundation\Concerns\ResolvesDumpSource; use Symfony\Component\ErrorHandler\Exception\FlattenException; +use function Illuminate\Filesystem\join_paths; + class Frame { use ResolvesDumpSource; @@ -37,20 +39,36 @@ class Frame */ protected $basePath; + /** + * The previous frame. + * + * @var \Illuminate\Foundation\Exceptions\Renderer\Frame|null + */ + protected $previous; + + /** + * Whether this frame is the main (first non-vendor) frame. + * + * @var bool + */ + protected $isMain = false; + /** * Create a new frame instance. * * @param \Symfony\Component\ErrorHandler\Exception\FlattenException $exception * @param array $classMap - * @param array{file: string, line: int, class?: string, type?: string, function?: string} $frame + * @param array{file: string, line: int, class?: string, type?: string, function?: string, args?: array} $frame * @param string $basePath + * @param \Illuminate\Foundation\Exceptions\Renderer\Frame|null $previous */ - public function __construct(FlattenException $exception, array $classMap, array $frame, string $basePath) + public function __construct(FlattenException $exception, array $classMap, array $frame, string $basePath, ?Frame $previous = null) { $this->exception = $exception; $this->classMap = $classMap; $this->frame = $frame; $this->basePath = $basePath; + $this->previous = $previous; } /** @@ -95,7 +113,11 @@ public function class() */ public function file() { - return str_replace($this->basePath.'/', '', $this->frame['file']); + return match (true) { + ! isset($this->frame['file']) => '[internal function]', + ! is_string($this->frame['file']) => '[unknown file]', + default => str_replace($this->basePath.DIRECTORY_SEPARATOR, '', $this->frame['file']), + }; } /** @@ -114,6 +136,16 @@ public function line() return $this->frame['line'] > $maxLines ? 1 : $this->frame['line']; } + /** + * Get the frame's function operator. + * + * @return '::'|'->'|'' + */ + public function operator() + { + return $this->frame['type']; + } + /** * Get the frame's function or method. * @@ -127,6 +159,27 @@ public function callable() }; } + /** + * Get the frame's arguments. + * + * @return array + */ + public function args() + { + if (! isset($this->frame['args']) || ! is_array($this->frame['args']) || count($this->frame['args']) === 0) { + return []; + } + + return array_map(function ($argument) { + [$key, $value] = $argument; + + return match ($key) { + 'object' => "{$key}({$value})", + default => $key, + }; + }, $this->frame['args']); + } + /** * Get the frame's code snippet. * @@ -155,6 +208,36 @@ public function snippet() public function isFromVendor() { return ! str_starts_with($this->frame['file'], $this->basePath) - || str_starts_with($this->frame['file'], $this->basePath.'/vendor'); + || str_starts_with($this->frame['file'], join_paths($this->basePath, 'vendor')); + } + + /** + * Get the previous frame. + * + * @return \Illuminate\Foundation\Exceptions\Renderer\Frame|null + */ + public function previous() + { + return $this->previous; + } + + /** + * Mark this frame as the main frame. + * + * @return void + */ + public function markAsMain() + { + $this->isMain = true; + } + + /** + * Determine if this is the main frame. + * + * @return bool + */ + public function isMain() + { + return $this->isMain; } } diff --git a/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php b/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php index d5a71b7d9178..3ab4a66680e0 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Listener.php @@ -59,7 +59,7 @@ public function queries() */ public function onQueryExecuted(QueryExecuted $event) { - if (count($this->queries) === 100) { + if (count($this->queries) === 101) { return; } @@ -67,7 +67,7 @@ public function onQueryExecuted(QueryExecuted $event) 'connectionName' => $event->connectionName, 'time' => $event->time, 'sql' => $event->sql, - 'bindings' => $event->bindings, + 'bindings' => $event->connection->prepareBindings($event->bindings), ]; } } diff --git a/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php b/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php index 514be710840e..cc464f3f9fbe 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\View\Factory; use Illuminate\Foundation\Exceptions\Renderer\Mappers\BladeMapper; use Illuminate\Http\Request; -use Illuminate\Support\Collection; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Throwable; @@ -89,8 +88,15 @@ public function render(Request $request, Throwable $throwable) $this->htmlErrorRenderer->render($throwable), ); + $exception = new Exception($flattenException, $request, $this->listener, $this->basePath); + + $exceptionAsMarkdown = $this->viewFactory->make('laravel-exceptions-renderer::markdown', [ + 'exception' => $exception, + ])->render(); + return $this->viewFactory->make('laravel-exceptions-renderer::show', [ - 'exception' => new Exception($flattenException, $request, $this->listener, $this->basePath), + 'exception' => $exception, + 'exceptionAsMarkdown' => $exceptionAsMarkdown, ])->render(); } @@ -101,19 +107,7 @@ public function render(Request $request, Throwable $throwable) */ public static function css() { - return (new Collection([ - ['styles.css', []], - ['light-mode.css', ['data-theme' => 'light']], - ['dark-mode.css', ['data-theme' => 'dark']], - ]))->map(function ($fileAndAttributes) { - [$filename, $attributes] = $fileAndAttributes; - - return ''; - })->implode(''); + return ''; } /** diff --git a/src/Illuminate/Foundation/Exceptions/views/minimal.blade.php b/src/Illuminate/Foundation/Exceptions/views/minimal.blade.php index db69f254f308..9140ab4e8e35 100644 --- a/src/Illuminate/Foundation/Exceptions/views/minimal.blade.php +++ b/src/Illuminate/Foundation/Exceptions/views/minimal.blade.php @@ -20,11 +20,11 @@
-
+
@yield('code')
-
+
@yield('message')
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/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index ae5fdc9c9c4e..3eccb5d7e85d 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -39,6 +39,7 @@ use Illuminate\Foundation\Console\ComponentMakeCommand; use Illuminate\Foundation\Console\ConfigCacheCommand; use Illuminate\Foundation\Console\ConfigClearCommand; +use Illuminate\Foundation\Console\ConfigMakeCommand; use Illuminate\Foundation\Console\ConfigPublishCommand; use Illuminate\Foundation\Console\ConfigShowCommand; use Illuminate\Foundation\Console\ConsoleMakeCommand; @@ -189,6 +190,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'ChannelMake' => ChannelMakeCommand::class, 'ClassMake' => ClassMakeCommand::class, 'ComponentMake' => ComponentMakeCommand::class, + 'ConfigMake' => ConfigMakeCommand::class, 'ConfigPublish' => ConfigPublishCommand::class, 'ConsoleMake' => ConsoleMakeCommand::class, 'ControllerMake' => ControllerMakeCommand::class, @@ -388,6 +390,18 @@ protected function registerConfigClearCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerConfigMakeCommand() + { + $this->app->singleton(ConfigMakeCommand::class, function ($app) { + return new ConfigMakeCommand($app['files']); + }); + } + /** * Register the command. * diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index fca2cc61057f..625e3f608bf3 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\Foundation\ExceptionRenderer; use Illuminate\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract; use Illuminate\Contracts\View\Factory; use Illuminate\Database\ConnectionInterface; @@ -70,7 +71,7 @@ public function boot() ], 'laravel-errors'); } - if ($this->app->hasDebugModeEnabled()) { + if ($this->app->hasDebugModeEnabled() && ! $this->app->has(ExceptionRenderer::class)) { $this->app->make(Listener::class)->registerListeners( $this->app->make(Dispatcher::class) ); @@ -210,13 +211,11 @@ protected function registerDeferHandler() $this->app->scoped(DeferredCallbackCollection::class); $this->app['events']->listen(function (CommandFinished $event) { - app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => app()->runningInConsole() && ($event->exitCode === 0 || $callback->always) - ); + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => app()->runningInConsole() && ($event->exitCode === 0 || $callback->always)); }); $this->app['events']->listen(function (JobAttempted $event) { - app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => $event->connectionName !== 'sync' && ($event->successful() || $callback->always) - ); + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => $event->connectionName !== 'sync' && ($event->successful() || $callback->always)); }); } diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index d5074505302d..ac7b3ec7838b 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -6,8 +6,8 @@ use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Events\DiscoverEvents; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Event; +use Illuminate\Support\LazyCollection; use Illuminate\Support\ServiceProvider; class EventServiceProvider extends ServiceProvider @@ -43,7 +43,7 @@ class EventServiceProvider extends ServiceProvider /** * The configured event discovery paths. * - * @var array|null + * @var iterable|null */ protected static $eventDiscoveryPaths; @@ -145,25 +145,23 @@ public function shouldDiscoverEvents() */ public function discoverEvents() { - return (new Collection($this->discoverEventsWithin())) + return (new LazyCollection($this->discoverEventsWithin())) ->flatMap(function ($directory) { return glob($directory, GLOB_ONLYDIR); }) ->reject(function ($directory) { return ! is_dir($directory); }) - ->reduce(function ($discovered, $directory) { - return array_merge_recursive( - $discovered, - DiscoverEvents::within($directory, $this->eventDiscoveryBasePath()) - ); - }, []); + ->pipe(fn ($directories) => DiscoverEvents::within( + $directories->all(), + $this->eventDiscoveryBasePath(), + )); } /** * Get the listener directories that should be used to discover events. * - * @return array + * @return iterable */ protected function discoverEventsWithin() { @@ -175,23 +173,24 @@ protected function discoverEventsWithin() /** * Add the given event discovery paths to the application's event discovery paths. * - * @param string|array $paths + * @param string|iterable $paths * @return void */ - public static function addEventDiscoveryPaths(array|string $paths) + public static function addEventDiscoveryPaths(iterable|string $paths) { - static::$eventDiscoveryPaths = array_values(array_unique( - array_merge(static::$eventDiscoveryPaths, Arr::wrap($paths)) - )); + static::$eventDiscoveryPaths = (new LazyCollection(static::$eventDiscoveryPaths)) + ->merge(is_string($paths) ? [$paths] : $paths) + ->unique() + ->values(); } /** * Set the globally configured event discovery paths. * - * @param array $paths + * @param iterable $paths * @return void */ - public static function setEventDiscoveryPaths(array $paths) + public static function setEventDiscoveryPaths(iterable $paths) { static::$eventDiscoveryPaths = $paths; } 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/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index c63a33164c25..ecaa4fdd38f4 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -36,9 +36,11 @@ trait InteractsWithContainer /** * Register an instance of an object in the container. * + * @template TSwap of object + * * @param string $abstract - * @param object $instance - * @return object + * @param TSwap $instance + * @return TSwap */ protected function swap($abstract, $instance) { @@ -48,9 +50,11 @@ protected function swap($abstract, $instance) /** * Register an instance of an object in the container. * + * @template TInstance of object + * * @param string $abstract - * @param object $instance - * @return object + * @param TInstance $instance + * @return TInstance */ protected function instance($abstract, $instance) { diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 89a0c3ac9797..73d5f9771b5b 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 iterable<\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 iterable<\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 iterable<\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 iterable<\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 iterable<\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 iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model * @return $this */ protected function assertModelMissing($model) @@ -194,22 +225,22 @@ protected function assertModelMissing($model) */ public function expectsDatabaseQueryCount($expected, $connection = null) { - with($this->getConnection($connection), function ($connectionInstance) use ($expected, $connection) { - $actual = 0; - - $connectionInstance->listen(function (QueryExecuted $event) use (&$actual, $connectionInstance, $connection) { - if (is_null($connection) || $connectionInstance === $event->connection) { - $actual++; - } - }); - - $this->beforeApplicationDestroyed(function () use (&$actual, $expected, $connectionInstance) { - $this->assertSame( - $expected, - $actual, - "Expected {$expected} database queries on the [{$connectionInstance->getName()}] connection. {$actual} occurred." - ); - }); + $connectionInstance = $this->getConnection($connection); + + $actual = 0; + + $connectionInstance->listen(function (QueryExecuted $event) use (&$actual, $connectionInstance, $connection) { + if (is_null($connection) || $connectionInstance === $event->connection) { + $actual++; + } + }); + + $this->beforeApplicationDestroyed(function () use (&$actual, $expected, $connectionInstance) { + $this->assertSame( + $expected, + $actual, + "Expected {$expected} database queries on the [{$connectionInstance->getName()}] connection. {$actual} occurred." + ); }); return $this; @@ -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(); } /** @@ -255,7 +285,7 @@ public function castAsJson($value, $connection = null) * Get the database connection. * * @param string|null $connection - * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string|null $table * @return \Illuminate\Database\Connection */ protected function getConnection($connection = null, $table = null) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php index f15814f1ea78..77b63302b653 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php @@ -105,6 +105,10 @@ public function setUpRedis() */ public function tearDownRedis() { + if (static::$connectionFailedOnceWithDefaultsSkip === true) { + return; + } + if (isset($this->redis['phpredis'])) { $this->redis['phpredis']->connection()->flushdb(); } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 4be085daa39c..0daabf1ce139 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -23,14 +23,17 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; +use Illuminate\Mail\Markdown; use Illuminate\Queue\Console\WorkCommand; use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Once; use Illuminate\Support\Sleep; use Illuminate\Support\Str; +use Illuminate\Validation\Validator; use Illuminate\View\Component; use Mockery; use Mockery\Exception\InvalidCountException; @@ -171,8 +174,10 @@ protected function tearDownTheTestEnvironment(): void Component::forgetFactory(); ConvertEmptyStringsToNull::flushState(); Factory::flushState(); + EncodedHtmlString::flushState(); EncryptCookies::flushState(); - HandleExceptions::flushState(); + HandleExceptions::flushState($this); + Markdown::flushState(); Migrator::withoutMigrations([]); Once::flush(); PreventRequestsDuringMaintenance::flushState(); @@ -183,6 +188,7 @@ protected function tearDownTheTestEnvironment(): void TrustProxies::flushState(); TrustHosts::flushState(); ValidateCsrfToken::flushState(); + Validator::flushState(); WorkCommand::flushState(); if ($this->callbackException) { 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/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index 9ed063241a8f..a84c082343b7 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; trait DatabaseTruncation @@ -120,7 +121,7 @@ protected function getAllTablesForConnection(ConnectionInterface $connection, ?s $schema = $connection->getSchemaBuilder(); - return static::$allTables[$name] = (new Collection($schema->getTables($schema->getCurrentSchemaListing())))->all(); + return static::$allTables[$name] = Arr::from($schema->getTables($schema->getCurrentSchemaListing())); } /** diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 7b7de434c27a..b348992c20f2 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, @@ -896,7 +897,7 @@ public function content($asset, $buildDirectory = null) $chunk = $this->chunk($this->manifest($buildDirectory), $asset); - $path = public_path($buildDirectory.'/'.$chunk['file']); + $path = $this->publicPath($buildDirectory.'/'.$chunk['file']); if (! is_file($path) || ! file_exists($path)) { throw new ViteException("Unable to locate file from Vite manifest: {$path}."); @@ -917,6 +918,17 @@ protected function assetPath($path, $secure = null) return ($this->assetPathResolver ?? asset(...))($path, $secure); } + /** + * Generate a public path for an asset. + * + * @param string $path + * @return string + */ + protected function publicPath($path) + { + return public_path($path); + } + /** * Get the manifest file for the given build directory. * diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index 8f0523dce09f..94ca5f84924b 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -1,8 +1,12 @@ action($name, $parameters, $absolute); } @@ -115,7 +127,6 @@ function action($name, $parameters = [], $absolute = true) * @template TClass of object * * @param string|class-string|null $abstract - * @param array $parameters * @return ($abstract is class-string ? TClass : ($abstract is null ? \Illuminate\Foundation\Application : mixed)) */ function app($abstract = null, array $parameters = []) @@ -133,9 +144,8 @@ function app($abstract = null, array $parameters = []) * Get the path to the application folder. * * @param string $path - * @return string */ - function app_path($path = '') + function app_path($path = ''): string { return app()->path($path); } @@ -147,9 +157,8 @@ function app_path($path = '') * * @param string $path * @param bool|null $secure - * @return string */ - function asset($path, $secure = null) + function asset($path, $secure = null): string { return app('url')->asset($path, $secure); } @@ -160,9 +169,9 @@ function asset($path, $secure = null) * Get the available auth instance. * * @param string|null $guard - * @return ($guard is null ? \Illuminate\Contracts\Auth\Factory : \Illuminate\Contracts\Auth\StatefulGuard) + * @return ($guard is null ? \Illuminate\Contracts\Auth\Factory : \Illuminate\Contracts\Auth\Guard) */ - function auth($guard = null) + function auth($guard = null): AuthFactory|Guard { if (is_null($guard)) { return app(AuthFactory::class); @@ -179,9 +188,8 @@ function auth($guard = null) * @param int $status * @param array $headers * @param mixed $fallback - * @return \Illuminate\Http\RedirectResponse */ - function back($status = 302, $headers = [], $fallback = false) + function back($status = 302, $headers = [], $fallback = false): RedirectResponse { return app('redirect')->back($status, $headers, $fallback); } @@ -192,9 +200,8 @@ function back($status = 302, $headers = [], $fallback = false) * Get the path to the base of the install. * * @param string $path - * @return string */ - function base_path($path = '') + function base_path($path = ''): string { return app()->basePath($path); } @@ -206,9 +213,8 @@ function base_path($path = '') * * @param string $value * @param array $options - * @return string */ - function bcrypt($value, $options = []) + function bcrypt($value, $options = []): string { return app('hash')->driver('bcrypt')->make($value, $options); } @@ -218,15 +224,48 @@ function bcrypt($value, $options = []) /** * Begin broadcasting an event. * - * @param mixed|null $event - * @return \Illuminate\Broadcasting\PendingBroadcast + * @param mixed $event */ - function broadcast($event = null) + function broadcast($event = null): PendingBroadcast { return app(BroadcastFactory::class)->event($event); } } +if (! function_exists('broadcast_if')) { + /** + * Begin broadcasting an event if the given condition is true. + * + * @param bool $boolean + * @param mixed $event + */ + function broadcast_if($boolean, $event = null): PendingBroadcast + { + if ($boolean) { + return app(BroadcastFactory::class)->event(value($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 $event + */ + function broadcast_unless($boolean, $event = null): PendingBroadcast + { + if (! $boolean) { + return app(BroadcastFactory::class)->event(value($event)); + } else { + return new FakePendingBroadcast; + } + } +} + if (! function_exists('cache')) { /** * Get / set the specified cache value. @@ -255,7 +294,7 @@ function cache($key = null, $default = null) ); } - return app('cache')->put(key($key), reset($key), ttl: $default); + return app('cache')->put(key($key), array_first($key), ttl: $default); } } @@ -288,9 +327,8 @@ function config($key = null, $default = null) * Get the configuration path. * * @param string $path - * @return string */ - function config_path($path = '') + function config_path($path = ''): string { return app()->configPath($path); } @@ -331,7 +369,7 @@ function context($key = null, $default = null) * @param string|null $sameSite * @return ($name is null ? \Illuminate\Cookie\CookieJar : \Symfony\Component\HttpFoundation\Cookie) */ - function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = null, $httpOnly = true, $raw = false, $sameSite = null) + function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = null, $httpOnly = true, $raw = false, $sameSite = null): CookieJar|Cookie { $cookie = app(CookieFactory::class); @@ -346,10 +384,8 @@ function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain if (! function_exists('csrf_field')) { /** * Generate a CSRF token form field. - * - * @return \Illuminate\Support\HtmlString */ - function csrf_field() + function csrf_field(): HtmlString { return new HtmlString(''); } @@ -359,11 +395,9 @@ function csrf_field() /** * Get the CSRF token value. * - * @return string - * * @throws \RuntimeException */ - function csrf_token() + function csrf_token(): ?string { $session = app('session'); @@ -380,9 +414,8 @@ function csrf_token() * Get the database path. * * @param string $path - * @return string */ - function database_path($path = '') + function database_path($path = ''): string { return app()->databasePath($path); } @@ -406,12 +439,9 @@ 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 + * @return ($callback is null ? \Illuminate\Support\Defer\DeferredCallbackCollection : \Illuminate\Support\Defer\DeferredCallback) */ - function defer(?callable $callback = null, ?string $name = null, bool $always = false) + function defer(?callable $callback = null, ?string $name = null, bool $always = false): DeferredCallback|DeferredCallbackCollection { return \Illuminate\Support\defer($callback, $name, $always); } @@ -424,7 +454,7 @@ function defer(?callable $callback = null, ?string $name = null, bool $always = * @param mixed $job * @return ($job is \Closure ? \Illuminate\Foundation\Bus\PendingClosureDispatch : \Illuminate\Foundation\Bus\PendingDispatch) */ - function dispatch($job) + function dispatch($job): PendingDispatch|PendingClosureDispatch { return $job instanceof Closure ? new PendingClosureDispatch(CallQueuedClosure::create($job)) @@ -454,9 +484,8 @@ function dispatch_sync($job, $handler = null) * * @param mixed $value * @param bool $serialize - * @return string */ - function encrypt($value, $serialize = true) + function encrypt($value, $serialize = true): string { return app('encrypter')->encrypt($value, $serialize); } @@ -482,9 +511,8 @@ function event(...$args) * Get a faker instance. * * @param string|null $locale - * @return \Faker\Generator */ - function fake($locale = null) + function fake($locale = null): \Faker\Generator { if (app()->bound('config')) { $locale ??= app('config')->get('app.faker_locale'); @@ -508,23 +536,33 @@ function fake($locale = null) * * @param string $message * @param array $context - * @return void */ - function info($message, $context = []) + function info($message, $context = []): void { app('log')->info($message, $context); } } +if (! function_exists('lang_path')) { + /** + * Get the path to the language folder. + * + * @param string $path + */ + function lang_path($path = ''): string + { + 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) + * @return ($message is null ? \Psr\Log\LoggerInterface : null) */ - function logger($message = null, array $context = []) + function logger($message = null, array $context = []): ?LoggerInterface { if (is_null($message)) { return app('log'); @@ -534,19 +572,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. @@ -554,7 +579,7 @@ function lang_path($path = '') * @param string|null $driver * @return ($driver is null ? \Illuminate\Log\LogManager : \Psr\Log\LoggerInterface) */ - function logs($driver = null) + function logs($driver = null): LoggerInterface|LogManager { return $driver ? app('log')->driver($driver) : app('log'); } @@ -565,9 +590,8 @@ function logs($driver = null) * Generate a form field to spoof the HTTP verb used by forms. * * @param string $method - * @return \Illuminate\Support\HtmlString */ - function method_field($method) + function method_field($method): HtmlString { return new HtmlString(''); } @@ -579,11 +603,10 @@ function method_field($method) * * @param string $path * @param string $manifestDirectory - * @return \Illuminate\Support\HtmlString|string * * @throws \Exception */ - function mix($path, $manifestDirectory = '') + function mix($path, $manifestDirectory = ''): HtmlString|string { return app(Mix::class)(...func_get_args()); } @@ -593,12 +616,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) + function now($tz = null): CarbonInterface { - return Date::now($tz); + return Date::now(enum_value($tz)); } } @@ -665,9 +688,8 @@ function precognitive($callable = null) * Get the path to the public folder. * * @param string $path - * @return string */ - function public_path($path = '') + function public_path($path = ''): string { return app()->publicPath($path); } @@ -683,7 +705,7 @@ function public_path($path = '') * @param bool|null $secure * @return ($to is null ? \Illuminate\Routing\Redirector : \Illuminate\Http\RedirectResponse) */ - function redirect($to = null, $status = 302, $headers = [], $secure = null) + function redirect($to = null, $status = 302, $headers = [], $secure = null): Redirector|RedirectResponse { if (is_null($to)) { return app('redirect'); @@ -698,9 +720,8 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null) * Report an exception. * * @param \Throwable|string $exception - * @return void */ - function report($exception) + function report($exception): void { if (is_string($exception)) { $exception = new Exception($exception); @@ -716,9 +737,8 @@ function report($exception) * * @param bool $boolean * @param \Throwable|string $exception - * @return void */ - function report_if($boolean, $exception) + function report_if($boolean, $exception): void { if ($boolean) { report($exception); @@ -732,9 +752,8 @@ function report_if($boolean, $exception) * * @param bool $boolean * @param \Throwable|string $exception - * @return void */ - function report_unless($boolean, $exception) + function report_unless($boolean, $exception): void { if (! $boolean) { report($exception); @@ -799,7 +818,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 = []) @@ -813,9 +831,8 @@ function resolve($name, array $parameters = []) * Get the path to the resources folder. * * @param string $path - * @return string */ - function resource_path($path = '') + function resource_path($path = ''): string { return app()->resourcePath($path); } @@ -827,10 +844,9 @@ 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 = []) + function response($content = null, $status = 200, array $headers = []): ResponseFactory|IlluminateResponse { $factory = app(ResponseFactory::class); @@ -849,9 +865,8 @@ function response($content = null, $status = 200, array $headers = []) * @param \BackedEnum|string $name * @param mixed $parameters * @param bool $absolute - * @return string */ - function route($name, $parameters = [], $absolute = true) + function route($name, $parameters = [], $absolute = true): string { return app('url')->route($name, $parameters, $absolute); } @@ -862,9 +877,8 @@ function route($name, $parameters = [], $absolute = true) * Generate an asset path for the application. * * @param string $path - * @return string */ - function secure_asset($path) + function secure_asset($path): string { return asset($path, true); } @@ -913,14 +927,29 @@ function session($key = null, $default = null) * Get the path to the storage folder. * * @param string $path - * @return string */ - function storage_path($path = '') + function storage_path($path = ''): string { return app()->storagePath($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 +970,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) + function today($tz = null): CarbonInterface { - return Date::today($tz); + return Date::today(enum_value($tz)); } } @@ -959,7 +988,7 @@ function today($tz = null) * @param string|null $locale * @return ($key is null ? \Illuminate\Contracts\Translation\Translator : array|string) */ - function trans($key = null, $replace = [], $locale = null) + function trans($key = null, $replace = [], $locale = null): Translator|array|string { if (is_null($key)) { return app('translator'); @@ -975,11 +1004,9 @@ 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 */ - function trans_choice($key, $number, array $replace = [], $locale = null) + function trans_choice($key, $number, array $replace = [], $locale = null): string { return app('translator')->choice($key, $number, $replace, $locale); } @@ -992,9 +1019,8 @@ function trans_choice($key, $number, array $replace = [], $locale = null) * @param string|null $key * @param array $replace * @param string|null $locale - * @return string|array|null */ - function __($key = null, $replace = [], $locale = null) + function __($key = null, $replace = [], $locale = null): string|array|null { if (is_null($key)) { return $key; @@ -1027,7 +1053,7 @@ function uri(UriInterface|Stringable|array|string $uri, mixed $parameters = [], * @param bool|null $secure * @return ($path is null ? \Illuminate\Contracts\Routing\UrlGenerator : string) */ - function url($path = null, $parameters = [], $secure = null) + function url($path = null, $parameters = [], $secure = null): UrlGenerator|string { if (is_null($path)) { return app(UrlGenerator::class); @@ -1041,13 +1067,9 @@ 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 = []) + function validator(?array $data = null, array $rules = [], array $messages = [], array $attributes = []): ValidatorContract|ValidationFactory { $factory = app(ValidationFactory::class); @@ -1068,7 +1090,7 @@ function validator(?array $data = null, array $rules = [], array $messages = [], * @param array $mergeData * @return ($view is null ? \Illuminate\Contracts\View\Factory : \Illuminate\Contracts\View\View) */ - function view($view = null, $data = [], $mergeData = []) + function view($view = null, $data = [], $mergeData = []): ViewFactory|ViewContract { $factory = app(ViewFactory::class); diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/.gitignore b/src/Illuminate/Foundation/resources/exceptions/renderer/.gitignore new file mode 100644 index 000000000000..07e6e472cc75 --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/badge.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/badge.blade.php new file mode 100644 index 000000000000..5fedfcf12535 --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/badge.blade.php @@ -0,0 +1,47 @@ +@props(['type' => 'default', 'variant' => 'soft']) + +@php +$baseClasses = 'inline-flex w-fit shrink-0 items-center justify-center gap-1 font-mono leading-3 uppercase transition-colors dark:border [&_svg]:size-2.5 h-6 min-w-5 rounded-md px-1.5 text-xs/none'; + +$types = [ + 'default' => [ + 'soft' => 'bg-black/8 text-neutral-900 dark:border-neutral-700 dark:bg-white/10 dark:text-neutral-100', + 'solid' => 'bg-neutral-600 text-neutral-100 dark:border-neutral-500 dark:bg-neutral-600', + ], + 'success' => [ + 'soft' => 'bg-emerald-200 text-emerald-900 dark:border-emerald-600 dark:bg-emerald-900/70 dark:text-emerald-400', + 'solid' => 'bg-emerald-600 dark:border-emerald-500 dark:bg-emerald-600', + ], + 'primary' => [ + 'soft' => 'bg-blue-100 text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300', + 'solid' => 'bg-blue-700 dark:border-blue-600 dark:bg-blue-700', + ], + 'error' => [ + 'soft' => 'bg-rose-200 text-rose-900 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-100 dark:[&_svg]:!text-white', + 'solid' => 'bg-rose-600 dark:border-rose-500 dark:bg-rose-600', + ], + 'alert' => [ + 'soft' => 'bg-amber-200 text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300', + 'solid' => 'bg-amber-600 dark:border-amber-500 dark:bg-amber-600', + ], + 'white' => [ + 'soft' => 'bg-white text-neutral-900 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100', + 'solid' => 'bg-black/10 text-neutral-900 dark:text-neutral-900 dark:bg-white', + ], +]; + +$variants = [ + 'soft' => '', + 'solid' => 'text-white dark:text-white [&_svg]:!text-white', +]; + +$typeClasses = $types[$type][$variant] ?? $types['default']['soft']; +$variantClasses = $variants[$variant] ?? $variants['soft']; + +$classes = implode(' ', [$baseClasses, $typeClasses, $variantClasses]); + +@endphp + +
merge(['class' => $classes]) }}> + {{ $slot }} +
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/card.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/card.blade.php deleted file mode 100644 index 14dcd4f51426..000000000000 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/card.blade.php +++ /dev/null @@ -1,5 +0,0 @@ -
merge(['class' => "@container flex flex-col p-6 sm:p-12 bg-white dark:bg-gray-900/80 text-gray-900 dark:text-gray-100 rounded-lg default:col-span-full default:lg:col-span-6 default:row-span-1 dark:ring-1 dark:ring-gray-800 shadow-xl"]) }} -> - {{ $slot }} -
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/context.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/context.blade.php deleted file mode 100644 index 13b922a865e6..000000000000 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/context.blade.php +++ /dev/null @@ -1,148 +0,0 @@ -@use('Illuminate\Support\Str') - -
- Request -
- -
- {{ $exception->request()->method() }} - {{ Str::start($exception->request()->path(), '/') }} -
- -
- Headers -
- -
- @forelse ($exception->requestHeaders() as $key => $value) -
- - {{ $key }} - - -
{{ $value }}
-
-
- @empty - -
No headers data
-
- @endforelse -
- -
- Body -
- -
-
- -
{{ $exception->requestBody() ?: 'No body data' }}
-
-
-
- -
- - -
- Application -
- -
- Routing -
- -
- @forelse ($exception->applicationRouteContext() as $name => $value) -
- {{ $name }} - -
{{ $value }}
-
-
- @empty - -
No routing data
-
- @endforelse -
- - @if ($routeParametersContext = $exception->applicationRouteParametersContext()) -
- Routing Parameters -
- -
-
- -
{{ $routeParametersContext }}
-
-
-
- @endif - -
- Database Queries - - @if (count($exception->applicationQueries()) === 100) - only the first 100 queries are displayed - @endif - -
- -
- @forelse ($exception->applicationQueries() as ['connectionName' => $connectionName, 'sql' => $sql, 'time' => $time]) -
-
- {{ $connectionName }} - -
- -
{{ $sql }}
-
-
- @empty - -
No query data
-
- @endforelse -
-
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/editor.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/editor.blade.php deleted file mode 100644 index 4171fb8e3e2d..000000000000 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/editor.blade.php +++ /dev/null @@ -1,32 +0,0 @@ -@foreach ($exception->frames() as $frame) -
-
-
-
- - @if (config('app.editor')) - - {{ $frame->file() }} - - @else - {{ $frame->file() }} - @endif - - :{{ $frame->line() }} -
-
-
-
-
-
-
-@endforeach diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/empty-state.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/empty-state.blade.php new file mode 100644 index 000000000000..9a41f78a8f1b --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/empty-state.blade.php @@ -0,0 +1,5 @@ +@props(['message']) + +
+ // {{ $message }} +
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/file-with-line.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/file-with-line.blade.php new file mode 100644 index 000000000000..99dd9f4a493b --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/file-with-line.blade.php @@ -0,0 +1,21 @@ +@props(['frame', 'direction' => 'ltr']) + +@php + $file = $frame->file(); + $line = $frame->line(); +@endphp + +
merge(['class' => 'truncate font-mono text-xs text-neutral-500 dark:text-neutral-400']) }} + dir="{{ $direction }}" +> + + @if (config('app.editor')) + + {{ $file }}:{{ $line }} + + @else + {{ $file }}:{{ $line }} + @endif + +
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/formatted-source.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/formatted-source.blade.php new file mode 100644 index 000000000000..e6364fc9eb05 --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/formatted-source.blade.php @@ -0,0 +1,23 @@ +@props(['frame']) + +@php + if ($class = $frame->class()) { + $source = $class; + + if ($previous = $frame->previous()) { + $source .= $previous->operator(); + $source .= $previous->callable(); + $source .= '('.implode(', ', $previous->args()).')'; + } + } else { + $source = $frame->source(); + } +@endphp + + diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/frame-code.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/frame-code.blade.php new file mode 100644 index 000000000000..23b022062825 --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/frame-code.blade.php @@ -0,0 +1,15 @@ +@props(['code', 'highlightedLine']) + +
+ +
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/frame.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/frame.blade.php new file mode 100644 index 000000000000..406c246aebdb --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/frame.blade.php @@ -0,0 +1,55 @@ +@props(['frame']) + +
+
+ {{-- Dot --}} +
+
+
+ +
+ + +
+ +
+ +
+
+ + @if($snippet = $frame->snippet()) + + @endif +
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/header.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/header.blade.php index 4da473e71d90..ffb0c9a9a212 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/header.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/header.blade.php @@ -1,28 +1,33 @@ - -
-
-
- - - {{ implode(' ', array_slice(explode('\\', $exception->class()), -1)) }} - -
-
- {{ $exception->message() }} -
-
+@props(['exception']) -