diff --git a/.gitattributes b/.gitattributes index ba7452152c0d..8382fc5c826f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,7 +17,7 @@ .gitattributes export-ignore .gitignore export-ignore .styleci.yml export-ignore -CHANGELOG-* export-ignore +CHANGELOG.md export-ignore CODE_OF_CONDUCT.md export-ignore CONTRIBUTING.md export-ignore docker-compose.yml export-ignore diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index 961af6983a22..bd20bfad48bd 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -293,53 +293,53 @@ jobs: DB_USERNAME: SA DB_PASSWORD: Forge123 - # mssql_2017: - # runs-on: ubuntu-20.04 - # timeout-minutes: 5 - - # services: - # sqlsrv: - # image: mcr.microsoft.com/mssql/server:2017-latest - # env: - # ACCEPT_EULA: Y - # SA_PASSWORD: Forge123 - # ports: - # - 1433:1433 - - # strategy: - # fail-fast: true - - # name: SQL Server 2017 - - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - - # - name: Setup PHP - # uses: shivammathur/setup-php@v2 - # with: - # php-version: 8.3 - # extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr - # tools: composer:v2 - # coverage: none - - # - name: Set Framework version - # run: composer config version "12.x-dev" - - # - name: Install dependencies - # uses: nick-fields/retry@v3 - # with: - # timeout_minutes: 5 - # max_attempts: 5 - # command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - - # - name: Execute tests - # run: vendor/bin/phpunit tests/Integration/Database - # env: - # DB_CONNECTION: sqlsrv - # DB_DATABASE: master - # DB_USERNAME: SA - # DB_PASSWORD: Forge123 + mssql_2017: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + services: + sqlsrv: + image: mcr.microsoft.com/mssql/server:2017-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: Forge123 + ports: + - 1433:1433 + + strategy: + fail-fast: true + + name: SQL Server 2017 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr + tools: composer:v2 + coverage: none + + - name: Set Framework version + run: composer config version "12.x-dev" + + - name: Install dependencies + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Database + env: + DB_CONNECTION: sqlsrv + DB_DATABASE: master + DB_USERNAME: SA + DB_PASSWORD: Forge123 sqlite: runs-on: ubuntu-24.04 diff --git a/.github/workflows/update-assets.yml b/.github/workflows/update-assets.yml index 8ecd63efce0a..0563172549a1 100644 --- a/.github/workflows/update-assets.yml +++ b/.github/workflows/update-assets.yml @@ -8,6 +8,9 @@ on: - '/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json' workflow_dispatch: +permissions: + contents: write + jobs: update: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index b292512610ef..6224987946d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,109 @@ # 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.14.1...12.x) + +## [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/composer.json b/composer.json index 708b0c12c893..8c76eb8b7c06 100644 --- a/composer.json +++ b/composer.json @@ -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", @@ -116,7 +116,7 @@ "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", @@ -189,7 +189,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/cache.php b/config/cache.php index 925f7d2ee84b..f529e1e3ec74 100644 --- a/config/cache.php +++ b/config/cache.php @@ -103,6 +103,6 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel'), '_').'_cache_'), ]; diff --git a/config/database.php b/config/database.php index 3e827c359b04..8a3b731fb52e 100644 --- a/config/database.php +++ b/config/database.php @@ -148,7 +148,7 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel'), '_').'_database_'), 'persistent' => env('REDIS_PERSISTENT', false), ], diff --git a/config/logging.php b/config/logging.php index 1345f6f66c51..9e998a496c86 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,7 +54,7 @@ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', 'single')), + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], diff --git a/config/mail.php b/config/mail.php index ff140eb439f8..22c03b032d76 100644 --- a/config/mail.php +++ b/config/mail.php @@ -46,7 +46,7 @@ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], 'ses' => [ diff --git a/config/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/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index 70723558886e..131959148b06 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -126,6 +126,7 @@ 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 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..3946e596ef9f 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), ); } diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 13bd15f46c5a..985b0bb4407c 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; } /** diff --git a/src/Illuminate/Bus/Queueable.php b/src/Illuminate/Bus/Queueable.php index c42614d9e65d..b8a439dad2ac 100644 --- a/src/Illuminate/Bus/Queueable.php +++ b/src/Illuminate/Bus/Queueable.php @@ -221,9 +221,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 +238,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/MemoizedStore.php b/src/Illuminate/Cache/MemoizedStore.php index d899ef09d609..fc6313db2a1a 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. @@ -160,6 +162,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/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index d9b7561db2cf..bea43ce76c26 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -4,9 +4,14 @@ use ArgumentCountError; use ArrayAccess; +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 +28,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. * @@ -378,6 +398,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. * diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 23e1af7bbfee..95faa17a7121 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -712,12 +712,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; } diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index d2894529ed6e..c11c9c434d89 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; @@ -1059,17 +1056,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/helpers.php b/src/Illuminate/Collections/helpers.php index 55844559e711..16c8f0118993 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -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, }; diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 07073aab309c..4729a5441a13 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; @@ -238,12 +240,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) { diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index 047577372b18..75cb579925cf 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -2,7 +2,6 @@ namespace Illuminate\Console\Scheduling; -use Exception; use Illuminate\Console\Application; use Illuminate\Console\Command; use Illuminate\Console\Events\ScheduledTaskFailed; @@ -197,10 +196,6 @@ protected function runEvent($event) round(microtime(true) - $start, 2) )); - if ($event->exitCode !== 0) { - 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/Container/Container.php b/src/Illuminate/Container/Container.php index 32ecaeabe998..5c401d465e7f 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1496,6 +1496,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 end($this->buildStack) ?: null; + } + /** * Get the container's bindings. * diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 4e22b9ae9fa2..ea5e095117c9 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', diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 0d0fc454bf0b..7c301ea31276 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -68,6 +68,13 @@ trait HasAttributes */ protected $changes = []; + /** + * The previous state of the changed model attributes. + * + * @var array + */ + protected $previous = []; + /** * The attributes that should be cast. * @@ -2082,6 +2089,7 @@ public function syncOriginalAttributes($attributes) public function syncChanges() { $this->changes = $this->getDirty(); + $this->previous = array_intersect_key($this->getRawOriginal(), $this->changes); return $this; } @@ -2117,7 +2125,7 @@ public function isClean($attributes = null) */ public function discardChanges() { - [$this->attributes, $this->changes] = [$this->original, []]; + [$this->attributes, $this->changes, $this->previous] = [$this->original, [], []]; return $this; } @@ -2201,6 +2209,16 @@ 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. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index f9f21536d1fc..b7955bd111a9 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -803,7 +803,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; 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/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/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/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 1f00b7bd655f..d2b97d5d121e 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 @@ -3016,7 +3019,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 +3033,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 */ @@ -3093,7 +3096,7 @@ public function soleValue($column) /** * 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 +3152,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 +3180,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 +3203,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,7 +3250,7 @@ protected function ensureOrderForCursorPagination($shouldReverse = false) /** * Get the count of the total records for the paginator. * - * @param array $columns + * @param array $columns * @return int */ public function getCountForPagination($columns = ['*']) @@ -3269,8 +3272,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 +3313,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) { @@ -3653,7 +3657,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 +3678,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) { @@ -4078,7 +4084,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 +4174,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 +4194,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/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 9b35083af603..ea1c44e00866 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) diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 109932a27d12..c22019536e7c 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(string, \Closure, string): \Illuminate\Database\Schema\Blueprint|null */ protected $resolver; @@ -698,7 +698,7 @@ public function getConnection() /** * Set the Schema Blueprint resolver callback. * - * @param \Closure $resolver + * @param \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint|null $resolver * @return void */ public function blueprintResolver(Closure $resolver) 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..7e1e6a1d2fa8 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -922,6 +922,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CURRENT_DATE')); + } + return 'date'; } @@ -1007,6 +1011,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); } 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..f0608eb2e4dc 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -769,6 +769,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 +860,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/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 30729f1ef2e8..1635de7742e5 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/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index ee409586efc8..c49a49d30ad7 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; @@ -631,8 +633,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); } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 9a18e7737177..243144794d59 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.14.1'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php index adabdcec0576..8574401b5d3e 100644 --- a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php +++ b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php @@ -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/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index f8506bc07dd4..e0ffdd534495 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); } 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..7b2e43c0c5be 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,6 +42,23 @@ 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. * @@ -54,25 +78,44 @@ 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, + ); + } } } @@ -118,8 +161,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 +179,171 @@ protected function enableBroadcastServiceProvider() } } + /** + * Collect the driver configuration. + * + * @return void + */ + protected function collectDriverConfig() + { + $envPath = $this->laravel->basePath('.env'); + + if (! file_exists($envPath)) { + return; + } + + match ($this->driver) { + 'pusher' => $this->collectPusherConfig(), + 'ably' => $this->collectAblyConfig(), + default => null, + }; + } + + /** + * Install the driver packages. + * + * @return void + */ + protected function installDriverPackages() + { + $package = match ($this->driver) { + 'pusher' => 'pusher/pusher-php-server', + 'ably' => 'ably/ably-php', + default => null, + }; + + if (! $package || InstalledVersions::isInstalled($package)) { + return; + } + + $this->requireComposerPackages($this->option('composer'), [$package]); + } + + /** + * Collect the Pusher configuration. + * + * @return void + */ + protected function collectPusherConfig() + { + $appId = text('Pusher App ID', 'Enter your Pusher app ID'); + $key = password('Pusher App Key', 'Enter your Pusher app key'); + $secret = password('Pusher App Secret', 'Enter your Pusher app secret'); + + $cluster = select('Pusher App Cluster', [ + 'mt1', + 'us2', + 'us3', + 'eu', + 'ap1', + 'ap2', + 'ap3', + 'ap4', + 'sa1', + ]); + + Env::writeVariables([ + 'PUSHER_APP_ID' => $appId, + 'PUSHER_APP_KEY' => $key, + 'PUSHER_APP_SECRET' => $secret, + 'PUSHER_APP_CLUSTER' => $cluster, + 'PUSHER_PORT' => 443, + 'PUSHER_SCHEME' => 'https', + 'VITE_PUSHER_APP_KEY' => '${PUSHER_APP_KEY}', + 'VITE_PUSHER_APP_CLUSTER' => '${PUSHER_APP_CLUSTER}', + 'VITE_PUSHER_HOST' => '${PUSHER_HOST}', + 'VITE_PUSHER_PORT' => '${PUSHER_PORT}', + 'VITE_PUSHER_SCHEME' => '${PUSHER_SCHEME}', + ], $this->laravel->basePath('.env')); + } + + /** + * Collect the Ably configuration. + * + * @return void + */ + protected function collectAblyConfig() + { + $this->components->warn('Make sure to enable "Pusher protocol support" in your Ably app settings.'); + + $key = password('Ably Key', 'Enter your Ably key'); + + $publicKey = explode(':', $key)[0] ?? $key; + + Env::writeVariables([ + 'ABLY_KEY' => $key, + 'ABLY_PUBLIC_KEY' => $publicKey, + 'VITE_ABLY_PUBLIC_KEY' => '${ABLY_PUBLIC_KEY}', + ], $this->laravel->basePath('.env')); + } + + /** + * Inject Echo configuration into the application's main file. + * + * @return void + */ + protected function injectFrameworkSpecificConfiguration() + { + if ($this->appUsesVue()) { + $importPath = $this->frameworkPackages['vue']; + + $filePaths = [ + $this->laravel->resourcePath('js/app.ts'), + $this->laravel->resourcePath('js/app.js'), + ]; + } else { + $importPath = $this->frameworkPackages['react']; + + $filePaths = [ + $this->laravel->resourcePath('js/app.tsx'), + $this->laravel->resourcePath('js/app.jsx'), + ]; + } + + $filePath = array_filter($filePaths, function ($path) { + return file_exists($path); + })[0] ?? null; + + if (! $filePath) { + $this->components->warn("Could not find file [{$filePaths[0]}]. Skipping automatic Echo configuration."); + + return; + } + + $contents = file_get_contents($filePath); + + $echoCode = <<driver}', + }); + JS; + + preg_match_all('/^import .+;$/m', $contents, $matches); + + if (empty($matches[0])) { + // Add the Echo configuration to the top of the file if no import statements are found... + $newContents = $echoCode.PHP_EOL.$contents; + + file_put_contents($filePath, $newContents); + } else { + // Add Echo configuration after the last import... + $lastImport = end($matches[0]); + + $positionOfLastImport = strrpos($contents, $lastImport); + + if ($positionOfLastImport !== false) { + $insertPosition = $positionOfLastImport + strlen($lastImport); + $newContents = substr($contents, 0, $insertPosition).PHP_EOL.$echoCode.substr($contents, $insertPosition); + + file_put_contents($filePath, $newContents); + } + } + + $this->components->info('Echo configuration added to ['.basename($filePath).'].'); + } + /** * Install Laravel Reverb into the application if desired. * @@ -141,7 +351,7 @@ protected function enableBroadcastServiceProvider() */ protected function installReverb() { - if ($this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { + if ($this->driver !== 'reverb' || $this->option('without-reverb') || InstalledVersions::isInstalled('laravel/reverb')) { return; } @@ -199,6 +409,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 +428,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/stubs/echo-js-ably.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub new file mode 100644 index 000000000000..ec518d214668 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js-ably.stub @@ -0,0 +1,13 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "pusher", + key: import.meta.env.VITE_ABLY_PUBLIC_KEY, + wsHost: "realtime-pusher.ably.io", + wsPort: 443, + disableStats: true, + encrypted: true, +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub new file mode 100644 index 000000000000..5a8a7f7e31ef --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/echo-js-pusher.stub @@ -0,0 +1,15 @@ +import Echo from 'laravel-echo'; + +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "pusher", + key: import.meta.env.VITE_PUSHER_APP_KEY, + cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER, + forceTLS: true, + wsHost: import.meta.env.VITE_PUSHER_HOST, + wsPort: import.meta.env.VITE_PUSHER_PORT, + wssPort: import.meta.env.VITE_PUSHER_PORT, + enabledTransports: ["ws", "wss"], +}); diff --git a/src/Illuminate/Foundation/Console/stubs/echo-js.stub b/src/Illuminate/Foundation/Console/stubs/echo-js-reverb.stub similarity index 100% rename from src/Illuminate/Foundation/Console/stubs/echo-js.stub rename to src/Illuminate/Foundation/Console/stubs/echo-js-reverb.stub diff --git a/src/Illuminate/Foundation/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/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/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 4be085daa39c..c372d55cc93f 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -23,9 +23,11 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; +use Illuminate\Mail\Markdown; use Illuminate\Queue\Console\WorkCommand; use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Once; @@ -171,8 +173,10 @@ protected function tearDownTheTestEnvironment(): void Component::forgetFactory(); ConvertEmptyStringsToNull::flushState(); Factory::flushState(); + EncodedHtmlString::flushState(); EncryptCookies::flushState(); HandleExceptions::flushState(); + Markdown::flushState(); Migrator::withoutMigrations([]); Once::flush(); PreventRequestsDuringMaintenance::flushState(); diff --git a/src/Illuminate/Foundation/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/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/package-lock.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json index 45eee2341a84..1935db7d4b58 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json @@ -11,7 +11,7 @@ "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "tippy.js": "^6.3.7", - "vite": "^5.4.18", + "vite": "^5.4.19", "vite-require": "^0.2.3" } }, @@ -2106,9 +2106,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "5.4.18", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", - "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/package.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package.json index efa13c77fc74..d9f2b42b685e 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package.json @@ -12,7 +12,7 @@ "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "tippy.js": "^6.3.7", - "vite": "^5.4.18", + "vite": "^5.4.19", "vite-require": "^0.2.3" } } diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index c38932ef33d5..49391a4fa1bb 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -62,14 +62,14 @@ class Factory /** * The recorded response array. * - * @var array + * @var list */ protected $recorded = []; /** * All created response sequences. * - * @var array + * @var list<\Illuminate\Http\Client\ResponseSequence> */ protected $responseSequences = []; @@ -195,7 +195,7 @@ public static function failedRequest($body = null, $status = 200, $headers = []) * Create a new connection exception for use during stubbing. * * @param string|null $message - * @return \GuzzleHttp\Promise\PromiseInterface + * @return \Closure(\Illuminate\Http\Client\Request): \GuzzleHttp\Promise\PromiseInterface */ public static function failedConnection($message = null) { @@ -221,7 +221,7 @@ public function sequence(array $responses = []) /** * Register a stub callable that will intercept requests and be able to return stub responses. * - * @param callable|array|null $callback + * @param callable|array|null $callback * @return $this */ public function fake($callback = null) @@ -283,7 +283,7 @@ public function fakeSequence($url = '*') * Stub the given URL using the given callback. * * @param string $url - * @param \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|callable|int|string|array $callback + * @param \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|callable|int|string|array|\Illuminate\Http\Client\ResponseSequence $callback * @return $this */ public function stubUrl($url, $callback) @@ -371,7 +371,7 @@ public function recordRequestResponsePair($request, $response) /** * Assert that a request / response pair was recorded matching a given truth test. * - * @param callable $callback + * @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback * @return void */ public function assertSent($callback) @@ -385,7 +385,7 @@ public function assertSent($callback) /** * Assert that the given request was sent in the given order. * - * @param array $callbacks + * @param list $callbacks * @return void */ public function assertSentInOrder($callbacks) @@ -407,7 +407,7 @@ public function assertSentInOrder($callbacks) /** * Assert that a request / response pair was not recorded matching a given truth test. * - * @param callable $callback + * @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback * @return void */ public function assertNotSent($callback) @@ -460,8 +460,8 @@ public function assertSequencesAreEmpty() /** * Get a collection of the request / response pairs matching the given truth test. * - * @param callable $callback - * @return \Illuminate\Support\Collection + * @param (\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool)|callable $callback + * @return \Illuminate\Support\Collection */ public function recorded($callback = null) { diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 117319de5029..7abcb3a459b8 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -26,7 +26,6 @@ use OutOfBoundsException; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; -use RuntimeException; use Symfony\Component\VarDumper\VarDumper; class PendingRequest @@ -1033,7 +1032,11 @@ protected function makePromise(string $method, string $url, array $options = [], $this->dispatchResponseReceivedEvent($response); }); }) - ->otherwise(function (OutOfBoundsException|TransferException $e) { + ->otherwise(function (OutOfBoundsException|TransferException|StrayRequestException $e) { + if ($e instanceof StrayRequestException) { + throw $e; + } + if ($e instanceof ConnectException || ($e instanceof RequestException && ! $e->hasResponse())) { $exception = new ConnectionException($e->getMessage(), 0, $e); @@ -1334,7 +1337,7 @@ public function buildStubHandler() if (is_null($response)) { if ($this->preventStrayRequests) { - throw new RuntimeException('Attempted request to ['.(string) $request->getUri().'] without a matching fake.'); + throw new StrayRequestException((string) $request->getUri()); } return $handler($request, $options); diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php index aba741f4bf75..e9716be08571 100644 --- a/src/Illuminate/Http/Client/Pool.php +++ b/src/Illuminate/Http/Client/Pool.php @@ -26,7 +26,7 @@ class Pool /** * The pool of requests. * - * @var array + * @var array */ protected $pool = []; @@ -65,7 +65,7 @@ protected function asyncRequest() /** * Retrieve the requests in the pool. * - * @return array + * @return array */ public function getRequests() { diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index c2352bb01f81..e69647bfb2bc 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -235,7 +235,7 @@ public function serverError() /** * Execute the given callback if there was a server or client error. * - * @param callable $callback + * @param callable|(\Closure(\Illuminate\Http\Client\Response): mixed) $callback * @return $this */ public function onError(callable $callback) @@ -339,7 +339,7 @@ public function throwIf($condition) /** * Throw an exception if the response status code matches the given code. * - * @param callable|int $statusCode + * @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode * @return $this * * @throws \Illuminate\Http\Client\RequestException @@ -357,7 +357,7 @@ public function throwIfStatus($statusCode) /** * Throw an exception unless the response status code matches the given code. * - * @param callable|int $statusCode + * @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode * @return $this * * @throws \Illuminate\Http\Client\RequestException diff --git a/src/Illuminate/Http/Client/StrayRequestException.php b/src/Illuminate/Http/Client/StrayRequestException.php new file mode 100644 index 000000000000..1392233bbadc --- /dev/null +++ b/src/Illuminate/Http/Client/StrayRequestException.php @@ -0,0 +1,13 @@ +header('Link', (new Collection(Vite::preloadedAssets())) + ->when($limit, fn ($assets, $limit) => $assets->take($limit)) ->map(fn ($attributes, $url) => "<{$url}>; ".implode('; ', $attributes)) ->join(', '), false); } diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index bee09601ef03..d64082868105 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -417,7 +417,7 @@ public function get(string $key, mixed $default = null): mixed * * @param string|null $key * @param mixed $default - * @return \Symfony\Component\HttpFoundation\InputBag|mixed + * @return ($key is null ? \Symfony\Component\HttpFoundation\InputBag : mixed) */ public function json($key = null, $default = null) { @@ -633,7 +633,7 @@ public function user($guard = null) * * @param string|null $param * @param mixed $default - * @return \Illuminate\Routing\Route|object|string|null + * @return ($param is null ? \Illuminate\Routing\Route : object|string|null) */ public function route($param = null, $default = null) { diff --git a/src/Illuminate/Log/Context/Repository.php b/src/Illuminate/Log/Context/Repository.php index 01e861925020..5fcfc2710e25 100644 --- a/src/Illuminate/Log/Context/Repository.php +++ b/src/Illuminate/Log/Context/Repository.php @@ -193,6 +193,28 @@ public function onlyHidden($keys) return array_intersect_key($this->hidden, array_flip($keys)); } + /** + * Retrieve all values except those with the given keys. + * + * @param array $keys + * @return array + */ + public function except($keys) + { + return array_diff_key($this->data, array_flip($keys)); + } + + /** + * Retrieve all hidden values except those with the given keys. + * + * @param array $keys + * @return array + */ + public function exceptHidden($keys) + { + return array_diff_key($this->hidden, array_flip($keys)); + } + /** * Add a context value. * diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index da69039adcb1..270b716f9d8d 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -628,6 +628,10 @@ protected function parseDriver($driver) $driver ??= 'null'; } + if ($driver === null) { + return null; + } + return trim($driver); } diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index afac565ec7a8..38318992c7f0 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -13,6 +13,7 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Support\Collection; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use Illuminate\Support\Traits\Conditionable; @@ -1371,7 +1372,7 @@ public function assertHasSubject($subject) */ public function assertSeeInHtml($string, $escape = true) { - $string = $escape ? e($string) : $string; + $string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string; [$html, $text] = $this->renderForAssertions(); @@ -1393,7 +1394,7 @@ public function assertSeeInHtml($string, $escape = true) */ public function assertDontSeeInHtml($string, $escape = true) { - $string = $escape ? e($string) : $string; + $string = $escape ? EncodedHtmlString::convert($string, withQuote: isset($this->markdown)) : $string; [$html, $text] = $this->renderForAssertions(); @@ -1415,7 +1416,9 @@ public function assertDontSeeInHtml($string, $escape = true) */ public function assertSeeInOrderInHtml($strings, $escape = true) { - $strings = $escape ? array_map(e(...), $strings) : $strings; + $strings = $escape ? array_map(function ($string) { + return EncodedHtmlString::convert($string, withQuote: isset($this->markdown)); + }, $strings) : $strings; [$html, $text] = $this->renderForAssertions(); diff --git a/src/Illuminate/Mail/Markdown.php b/src/Illuminate/Mail/Markdown.php index 06d123ed3858..d99a232b7f0b 100644 --- a/src/Illuminate/Mail/Markdown.php +++ b/src/Illuminate/Mail/Markdown.php @@ -3,6 +3,7 @@ namespace Illuminate\Mail; use Illuminate\Contracts\View\Factory as ViewFactory; +use Illuminate\Support\EncodedHtmlString; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use League\CommonMark\Environment\Environment; @@ -34,6 +35,13 @@ class Markdown */ protected $componentPaths = []; + /** + * Indicates if secure encoding should be enabled. + * + * @var bool + */ + protected static $withSecuredEncoding = false; + /** * Create a new Markdown renderer instance. * @@ -59,9 +67,37 @@ public function render($view, array $data = [], $inliner = null) { $this->view->flushFinderCache(); - $contents = $this->view->replaceNamespace( - 'mail', $this->htmlComponentPaths() - )->make($view, $data)->render(); + $bladeCompiler = $this->view + ->getEngineResolver() + ->resolve('blade') + ->getCompiler(); + + $contents = $bladeCompiler->usingEchoFormat( + 'new \Illuminate\Support\EncodedHtmlString(%s)', + function () use ($view, $data) { + if (static::$withSecuredEncoding === true) { + EncodedHtmlString::encodeUsing(function ($value) { + $replacements = [ + '[' => '\[', + '<' => '<', + '>' => '>', + ]; + + return str_replace(array_keys($replacements), array_values($replacements), $value); + }); + } + + try { + $contents = $this->view->replaceNamespace( + 'mail', $this->htmlComponentPaths() + )->make($view, $data)->render(); + } finally { + EncodedHtmlString::flushState(); + } + + return $contents; + } + ); if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) { $theme = $customTheme; @@ -72,7 +108,7 @@ public function render($view, array $data = [], $inliner = null) } return new HtmlString(($inliner ?: new CssToInlineStyles)->convert( - $contents, $this->view->make($theme, $data)->render() + str_replace('\[', '[', $contents), $this->view->make($theme, $data)->render() )); } @@ -100,20 +136,59 @@ public function renderText($view, array $data = []) * Parse the given Markdown text into HTML. * * @param string $text + * @param bool $encoded * @return \Illuminate\Support\HtmlString */ - public static function parse($text) + public static function parse($text, bool $encoded = false) { - $environment = new Environment([ + if ($encoded === false) { + return new HtmlString(static::converter()->convert($text)->getContent()); + } + + if (static::$withSecuredEncoding === true || $encoded === true) { + EncodedHtmlString::encodeUsing(function ($value) { + $replacements = [ + '[' => '\[', + '<' => '\<', + ]; + + $html = str_replace(array_keys($replacements), array_values($replacements), $value); + + return static::converter([ + 'html_input' => 'escape', + ])->convert($html)->getContent(); + }); + } + + $html = ''; + + try { + $html = static::converter()->convert($text)->getContent(); + } finally { + EncodedHtmlString::flushState(); + } + + return new HtmlString($html); + } + + /** + * Get a Markdown converter instance. + * + * @internal + * + * @param array $config + * @return \League\CommonMark\MarkdownConverter + */ + public static function converter(array $config = []) + { + $environment = new Environment(array_merge([ 'allow_unsafe_links' => false, - ]); + ], $config)); $environment->addExtension(new CommonMarkCoreExtension); $environment->addExtension(new TableExtension); - $converter = new MarkdownConverter($environment); - - return new HtmlString($converter->convert($text)->getContent()); + return new MarkdownConverter($environment); } /** @@ -185,4 +260,34 @@ public function getTheme() { return $this->theme; } + + /** + * Enable secured encoding when parsing Markdown. + * + * @return void + */ + public static function withSecuredEncoding() + { + static::$withSecuredEncoding = true; + } + + /** + * Disable secured encoding when parsing Markdown. + * + * @return void + */ + public static function withoutSecuredEncoding() + { + static::$withSecuredEncoding = false; + } + + /** + * Flush the class's global state. + * + * @return void + */ + public static function flushState() + { + static::$withSecuredEncoding = false; + } } diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index cf28958fdcad..6f976a9e45dc 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -20,7 +20,7 @@ "illuminate/contracts": "^12.0", "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", - "league/commonmark": "^2.6", + "league/commonmark": "^2.7", "psr/log": "^1.0|^2.0|^3.0", "symfony/mailer": "^7.2.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5" diff --git a/src/Illuminate/Mail/resources/views/html/button.blade.php b/src/Illuminate/Mail/resources/views/html/button.blade.php index 4a9bf7d00495..050e969d2130 100644 --- a/src/Illuminate/Mail/resources/views/html/button.blade.php +++ b/src/Illuminate/Mail/resources/views/html/button.blade.php @@ -12,7 +12,7 @@
-{{ $slot }} +{!! $slot !!}
diff --git a/src/Illuminate/Mail/resources/views/html/header.blade.php b/src/Illuminate/Mail/resources/views/html/header.blade.php index 56197f8d23f3..c47a260c56b2 100644 --- a/src/Illuminate/Mail/resources/views/html/header.blade.php +++ b/src/Illuminate/Mail/resources/views/html/header.blade.php @@ -5,7 +5,7 @@ @if (trim($slot) === 'Laravel') @else -{{ $slot }} +{!! $slot !!} @endif diff --git a/src/Illuminate/Mail/resources/views/html/layout.blade.php b/src/Illuminate/Mail/resources/views/html/layout.blade.php index d31a01de8630..0fa6b82f72b2 100644 --- a/src/Illuminate/Mail/resources/views/html/layout.blade.php +++ b/src/Illuminate/Mail/resources/views/html/layout.blade.php @@ -23,7 +23,7 @@ } } -{{ $head ?? '' }} +{!! $head ?? '' !!} @@ -31,7 +31,7 @@ -{{ $header ?? '' }} +{!! $header ?? '' !!} @@ -40,16 +40,16 @@ -{{ $footer ?? '' }} +{!! $footer ?? '' !!} diff --git a/src/Illuminate/Mail/resources/views/html/message.blade.php b/src/Illuminate/Mail/resources/views/html/message.blade.php index 1a874fc26de5..a16bace0a691 100644 --- a/src/Illuminate/Mail/resources/views/html/message.blade.php +++ b/src/Illuminate/Mail/resources/views/html/message.blade.php @@ -7,13 +7,13 @@ {{-- Body --}} -{{ $slot }} +{!! $slot !!} {{-- Subcopy --}} @isset($subcopy) -{{ $subcopy }} +{!! $subcopy !!} @endisset diff --git a/src/Illuminate/Queue/CallQueuedClosure.php b/src/Illuminate/Queue/CallQueuedClosure.php index 732600ccfea1..34bdf3b85796 100644 --- a/src/Illuminate/Queue/CallQueuedClosure.php +++ b/src/Illuminate/Queue/CallQueuedClosure.php @@ -22,6 +22,13 @@ class CallQueuedClosure implements ShouldQueue */ public $closure; + /** + * The name assigned to the job. + * + * @var string|null + */ + public $name = null; + /** * The callbacks that should be executed on failure. * @@ -105,6 +112,21 @@ public function displayName() { $reflection = new ReflectionFunction($this->closure->getClosure()); - return 'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; + $prefix = is_null($this->name) ? '' : "{$this->name} - "; + + return $prefix.'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; + } + + /** + * Assign a name to the job. + * + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + + return $this; } } diff --git a/src/Illuminate/Queue/CallQueuedHandler.php b/src/Illuminate/Queue/CallQueuedHandler.php index 0da7e735ca39..c7b887a4d953 100644 --- a/src/Illuminate/Queue/CallQueuedHandler.php +++ b/src/Illuminate/Queue/CallQueuedHandler.php @@ -274,12 +274,17 @@ protected function ensureUniqueJobLockIsReleasedViaContext() * @param array $data * @param \Throwable|null $e * @param string $uuid + * @param \Illuminate\Contracts\Queue\Job|null $job * @return void */ - public function failed(array $data, $e, string $uuid) + public function failed(array $data, $e, string $uuid, ?Job $job = null) { $command = $this->getCommand($data); + if (! is_null($job)) { + $command = $this->setJobInstanceIfNecessary($job, $command); + } + if (! $command instanceof ShouldBeUniqueUntilProcessing) { $this->ensureUniqueJobLockIsReleased($command); } diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index 8ec6ac54f805..112501b26580 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -251,7 +251,7 @@ protected function failed($e) [$class, $method] = JobName::parse($payload['job']); if (method_exists($this->instance = $this->resolve($class), 'failed')) { - $this->instance->failed($payload['data'], $e, $payload['uuid'] ?? ''); + $this->instance->failed($payload['data'], $e, $payload['uuid'] ?? '', $this); } } diff --git a/src/Illuminate/Queue/Middleware/RateLimited.php b/src/Illuminate/Queue/Middleware/RateLimited.php index 1b4b2b78d941..a2b5343e59db 100644 --- a/src/Illuminate/Queue/Middleware/RateLimited.php +++ b/src/Illuminate/Queue/Middleware/RateLimited.php @@ -25,6 +25,13 @@ class RateLimited */ protected $limiterName; + /** + * The number of seconds before a job should be available again if the limit is exceeded. + * + * @var \DateTimeInterface|int|null + */ + public $releaseAfter; + /** * Indicates if the job should be released if the limit is exceeded. * @@ -89,7 +96,7 @@ protected function handleJob($job, $next, array $limits) foreach ($limits as $limit) { if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) { return $this->shouldRelease - ? $job->release($this->getTimeUntilNextRetry($limit->key)) + ? $job->release($this->releaseAfter ?: $this->getTimeUntilNextRetry($limit->key)) : false; } @@ -99,6 +106,19 @@ protected function handleJob($job, $next, array $limits) return $next($job); } + /** + * Set the delay (in seconds) to release the job back to the queue. + * + * @param \DateTimeInterface|int $releaseAfter + * @return $this + */ + public function releaseAfter($releaseAfter) + { + $this->releaseAfter = $releaseAfter; + + return $this; + } + /** * Do not release the job back to the queue if the limit is exceeded. * diff --git a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php index bb87b101d404..25870e08f034 100644 --- a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php +++ b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php @@ -50,7 +50,7 @@ protected function handleJob($job, $next, array $limits) foreach ($limits as $limit) { if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) { return $this->shouldRelease - ? $job->release($this->getTimeUntilNextRetry($limit->key)) + ? $job->release($this->releaseAfter ?: $this->getTimeUntilNextRetry($limit->key)) : false; } } diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 68017795655c..5c5c7d04a7ed 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -57,6 +57,13 @@ class ThrottlesExceptions */ protected $whenCallback; + /** + * The callbacks that determine if the job should be deleted. + * + * @var callable[] + */ + protected array $deleteWhenCallbacks = []; + /** * The prefix of the rate limiter key. * @@ -111,6 +118,10 @@ public function handle($job, $next) report($throwable); } + if ($this->shouldDelete($throwable)) { + return $job->delete(); + } + $this->limiter->hit($jobKey, $this->decaySeconds); return $job->release($this->retryAfterMinutes * 60); @@ -130,6 +141,38 @@ public function when(callable $callback) return $this; } + /** + * Add a callback that should determine if the job should be deleted. + * + * @param callable|string $callback + * @return $this + */ + public function deleteWhen(callable|string $callback) + { + $this->deleteWhenCallbacks[] = is_string($callback) + ? fn (Throwable $e) => $e instanceof $callback + : $callback; + + return $this; + } + + /** + * Run the skip / delete callbacks to determine if the job should be deleted for the given exception. + * + * @param Throwable $throwable + * @return bool + */ + protected function shouldDelete(Throwable $throwable): bool + { + foreach ($this->deleteWhenCallbacks as $callback) { + if (call_user_func($callback, $throwable)) { + return true; + } + } + + return false; + } + /** * Set the prefix of the rate limiter key. * diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php index e5b79a7d67ed..8c6b78912b0d 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -58,6 +58,10 @@ public function handle($job, $next) report($throwable); } + if ($this->shouldDelete($throwable)) { + return $job->delete(); + } + $this->limiter->acquire(); return $job->release($this->retryAfterMinutes * 60); diff --git a/src/Illuminate/Queue/SerializesModels.php b/src/Illuminate/Queue/SerializesModels.php index db17b98755af..388c1d745561 100644 --- a/src/Illuminate/Queue/SerializesModels.php +++ b/src/Illuminate/Queue/SerializesModels.php @@ -36,6 +36,10 @@ public function __serialize() continue; } + if (method_exists($property, 'isVirtual') && $property->isVirtual()) { + continue; + } + $value = $this->getPropertyValue($property); if ($property->hasDefaultValue() && $value === $property->getDefaultValue()) { diff --git a/src/Illuminate/Redis/composer.json b/src/Illuminate/Redis/composer.json index 3addbb07ff68..05535a2460b2 100755 --- a/src/Illuminate/Redis/composer.json +++ b/src/Illuminate/Redis/composer.json @@ -27,7 +27,7 @@ }, "suggest": { "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", - "predis/predis": "Required to use the predis connector (^2.3)." + "predis/predis": "Required to use the predis connector (^2.3|^3.0)." }, "extra": { "branch-alias": { diff --git a/src/Illuminate/Routing/RouteUrlGenerator.php b/src/Illuminate/Routing/RouteUrlGenerator.php index cd23162c015f..52fcec1e4237 100644 --- a/src/Illuminate/Routing/RouteUrlGenerator.php +++ b/src/Illuminate/Routing/RouteUrlGenerator.php @@ -197,9 +197,14 @@ protected function formatParameters(Route $route, $parameters) unset($parameters[$name]); continue; - } elseif (! isset($this->defaultParameters[$name]) && ! isset($optionalParameters[$name])) { - // No named parameter or default value for a required parameter, try to match to positional parameter below... - array_push($requiredRouteParametersWithoutDefaultsOrNamedParameters, $name); + } else { + $bindingField = $route->bindingFieldFor($name); + $defaultParameterKey = $bindingField ? "$name:$bindingField" : $name; + + if (! isset($this->defaultParameters[$defaultParameterKey]) && ! isset($optionalParameters[$name])) { + // No named parameter or default value for a required parameter, try to match to positional parameter below... + array_push($requiredRouteParametersWithoutDefaultsOrNamedParameters, $name); + } } $namedParameters[$name] = ''; diff --git a/src/Illuminate/Support/EncodedHtmlString.php b/src/Illuminate/Support/EncodedHtmlString.php new file mode 100644 index 000000000000..a25115740277 --- /dev/null +++ b/src/Illuminate/Support/EncodedHtmlString.php @@ -0,0 +1,101 @@ +html; + + if ($value instanceof DeferringDisplayableValue) { + $value = $value->resolveDisplayableValue(); + } + + if ($value instanceof Htmlable) { + return $value->toHtml(); + } + + if ($value instanceof BackedEnum) { + $value = $value->value; + } + + return (static::$encodeUsingFactory ?? function ($value, $doubleEncode) { + return static::convert($value, doubleEncode: $doubleEncode); + })($value, $this->doubleEncode); + } + + /** + * Set the callable that will be used to encode the HTML strings. + * + * @param callable|null $factory + * @return void + */ + public static function encodeUsing(?callable $factory = null) + { + static::$encodeUsingFactory = $factory; + } + + /** + * Flush the class's global state. + * + * @return void + */ + public static function flushState() + { + static::$encodeUsingFactory = null; + } +} diff --git a/src/Illuminate/Support/Env.php b/src/Illuminate/Support/Env.php index 702f61d44f4c..a52c10bd4f2a 100644 --- a/src/Illuminate/Support/Env.php +++ b/src/Illuminate/Support/Env.php @@ -5,6 +5,8 @@ use Closure; use Dotenv\Repository\Adapter\PutenvAdapter; use Dotenv\Repository\RepositoryBuilder; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; use PhpOption\Option; use RuntimeException; @@ -114,6 +116,133 @@ public static function getOrFail($key) return self::getOption($key)->getOrThrow(new RuntimeException("Environment variable [$key] has no value.")); } + /** + * Write an array of key-value pairs to the environment file. + * + * @param array $variables + * @param string $pathToFile + * @param bool $overwrite + * @return void + * + * @throws RuntimeException + * @throws FileNotFoundException + */ + public static function writeVariables(array $variables, string $pathToFile, bool $overwrite = false): void + { + $filesystem = new Filesystem; + + if ($filesystem->missing($pathToFile)) { + throw new RuntimeException("The file [{$pathToFile}] does not exist."); + } + + $lines = explode(PHP_EOL, $filesystem->get($pathToFile)); + + foreach ($variables as $key => $value) { + $lines = self::addVariableToEnvContents($key, $value, $lines, $overwrite); + } + + $filesystem->put($pathToFile, implode(PHP_EOL, $lines)); + } + + /** + * Write a single key-value pair to the environment file. + * + * @param string $key + * @param mixed $value + * @param string $pathToFile + * @param bool $overwrite + * @return void + * + * @throws RuntimeException + * @throws FileNotFoundException + */ + public static function writeVariable(string $key, mixed $value, string $pathToFile, bool $overwrite = false): void + { + $filesystem = new Filesystem; + + if ($filesystem->missing($pathToFile)) { + throw new RuntimeException("The file [{$pathToFile}] does not exist."); + } + + $envContent = $filesystem->get($pathToFile); + + $lines = explode(PHP_EOL, $envContent); + $lines = self::addVariableToEnvContents($key, $value, $lines, $overwrite); + + $filesystem->put($pathToFile, implode(PHP_EOL, $lines)); + } + + /** + * Add a variable to the environment file contents. + * + * @param string $key + * @param mixed $value + * @param array $envLines + * @param bool $overwrite + * @return array + */ + protected static function addVariableToEnvContents(string $key, mixed $value, array $envLines, bool $overwrite): array + { + $prefix = explode('_', $key)[0].'_'; + $lastPrefixIndex = -1; + + $shouldQuote = preg_match('/^[a-zA-z0-9]+$/', $value) === 0; + + $lineToAddVariations = [ + $key.'='.(is_string($value) ? '"'.addslashes($value).'"' : $value), + $key.'='.(is_string($value) ? "'".addslashes($value)."'" : $value), + $key.'='.$value, + ]; + + $lineToAdd = $shouldQuote ? $lineToAddVariations[0] : $lineToAddVariations[2]; + + if ($value === '') { + $lineToAdd = $key.'='; + } + + foreach ($envLines as $index => $line) { + if (str_starts_with($line, $prefix)) { + $lastPrefixIndex = $index; + } + + if (in_array($line, $lineToAddVariations)) { + // This exact line already exists, so we don't need to add it again. + return $envLines; + } + + if ($line === $key.'=') { + // If the value is empty, we can replace it with the new value. + $envLines[$index] = $lineToAdd; + + return $envLines; + } + + if (str_starts_with($line, $key.'=')) { + if (! $overwrite) { + return $envLines; + } + + $envLines[$index] = $lineToAdd; + + return $envLines; + } + } + + if ($lastPrefixIndex === -1) { + if (count($envLines) && $envLines[count($envLines) - 1] !== '') { + $envLines[] = ''; + } + + return array_merge($envLines, [$lineToAdd]); + } + + return array_merge( + array_slice($envLines, 0, $lastPrefixIndex + 1), + [$lineToAdd], + array_slice($envLines, $lastPrefixIndex + 1) + ); + } + /** * Get the possible option for this environment variable. * diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index ed8a9d6dce10..9dd93084dad4 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -130,6 +130,7 @@ * @method static void afterResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void afterResolvingAttribute(string $attribute, \Closure $callback) * @method static void fireAfterResolvingAttributeCallbacks(\ReflectionAttribute[] $attributes, mixed $object) + * @method static string|null currentlyResolving() * @method static array getBindings() * @method static string getAlias(string $abstract) * @method static void forgetExtenders(string $abstract) diff --git a/src/Illuminate/Support/Facades/Blade.php b/src/Illuminate/Support/Facades/Blade.php index 01dc7ae76723..8ca0ca8c0249 100755 --- a/src/Illuminate/Support/Facades/Blade.php +++ b/src/Illuminate/Support/Facades/Blade.php @@ -31,6 +31,7 @@ * @method static array getCustomDirectives() * @method static \Illuminate\View\Compilers\BladeCompiler prepareStringsForCompilationUsing(callable $callback) * @method static void precompiler(callable $precompiler) + * @method static string usingEchoFormat(string $format, callable $callback) * @method static void setEchoFormat(string $format) * @method static void withDoubleEncoding() * @method static void withoutDoubleEncoding() diff --git a/src/Illuminate/Support/Facades/Context.php b/src/Illuminate/Support/Facades/Context.php index 6b894fad39b7..aaac04d8ed00 100644 --- a/src/Illuminate/Support/Facades/Context.php +++ b/src/Illuminate/Support/Facades/Context.php @@ -15,6 +15,8 @@ * @method static mixed pullHidden(string $key, mixed $default = null) * @method static array only(array $keys) * @method static array onlyHidden(array $keys) + * @method static array except(array $keys) + * @method static array exceptHidden(array $keys) * @method static \Illuminate\Log\Context\Repository add(string|array $key, mixed $value = null) * @method static \Illuminate\Log\Context\Repository addHidden(string|array $key, mixed $value = null) * @method static \Illuminate\Log\Context\Repository forget(string|array $key) diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index ecbcca5ba49c..823056859572 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -12,19 +12,19 @@ * @method static \GuzzleHttp\Promise\PromiseInterface response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \GuzzleHttp\Psr7\Response psr7Response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \Illuminate\Http\Client\RequestException failedRequest(array|string|null $body = null, int $status = 200, array $headers = []) - * @method static \GuzzleHttp\Promise\PromiseInterface failedConnection(string|null $message = null) + * @method static \Closure failedConnection(string|null $message = null) * @method static \Illuminate\Http\Client\ResponseSequence sequence(array $responses = []) * @method static bool preventingStrayRequests() * @method static \Illuminate\Http\Client\Factory allowStrayRequests() * @method static \Illuminate\Http\Client\Factory record() * @method static void recordRequestResponsePair(\Illuminate\Http\Client\Request $request, \Illuminate\Http\Client\Response|null $response) - * @method static void assertSent(callable $callback) + * @method static void assertSent(callable|\Closure $callback) * @method static void assertSentInOrder(array $callbacks) - * @method static void assertNotSent(callable $callback) + * @method static void assertNotSent(callable|\Closure $callback) * @method static void assertNothingSent() * @method static void assertSentCount(int $count) * @method static void assertSequencesAreEmpty() - * @method static \Illuminate\Support\Collection recorded(callable $callback = null) + * @method static \Illuminate\Support\Collection recorded(\Closure|callable $callback = null) * @method static \Illuminate\Http\Client\PendingRequest createPendingRequest() * @method static \Illuminate\Contracts\Events\Dispatcher|null getDispatcher() * @method static array getGlobalMiddleware() diff --git a/src/Illuminate/Support/Facades/Password.php b/src/Illuminate/Support/Facades/Password.php index 7099e3971fd8..ac6f226aa251 100755 --- a/src/Illuminate/Support/Facades/Password.php +++ b/src/Illuminate/Support/Facades/Password.php @@ -15,6 +15,7 @@ * @method static void deleteToken(\Illuminate\Contracts\Auth\CanResetPassword $user) * @method static bool tokenExists(\Illuminate\Contracts\Auth\CanResetPassword $user, string $token) * @method static \Illuminate\Auth\Passwords\TokenRepositoryInterface getRepository() + * @method static \Illuminate\Support\Timebox getTimebox() * * @see \Illuminate\Auth\Passwords\PasswordBrokerManager * @see \Illuminate\Auth\Passwords\PasswordBroker diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 8461fa41b1ee..f2dde82dc1c2 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -151,7 +151,7 @@ * @method static string|array|null cookie(string|null $key = null, string|array|null $default = null) * @method static array allFiles() * @method static bool hasFile(string $key) - * @method static array|(\Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|null file(string|null $key = null, mixed $default = null) + * @method static array|\Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|null file(string|null $key = null, mixed $default = null) * @method static \Illuminate\Http\Request dump(mixed $keys = []) * @method static never dd(mixed ...$args) * @method static bool exists(string|array $key) @@ -170,7 +170,7 @@ * @method static int integer(string $key, int $default = 0) * @method static float float(string $key, float $default = 0) * @method static \Illuminate\Support\Carbon|null date(string $key, string|null $format = null, string|null $tz = null) - * @method static \BackedEnum|null enum(string $key, string $enumClass) + * @method static \BackedEnum|null enum(string $key, string $enumClass, \BackedEnum|null $default = null) * @method static \BackedEnum[] enums(string $key, string $enumClass) * @method static array array(array|string|null $key = null) * @method static \Illuminate\Support\Collection collect(array|string|null $key = null) diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 09d0844c8610..d0e3e5f84bf1 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -44,7 +44,7 @@ * @method static string|null getCurrentSchemaName() * @method static array parseSchemaAndTable(string $reference, string|bool|null $withDefaultSchema = null) * @method static \Illuminate\Database\Connection getConnection() - * @method static void blueprintResolver(\Closure $resolver) + * @method static void blueprintResolver(\Closure|null $resolver) * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) * @method static bool hasMacro(string $name) diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index ec5d6342d416..f4a642f7df6c 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -48,6 +48,47 @@ public static function format(int|float $number, ?int $precision = null, ?int $m return $formatter->format($number); } + /** + * Parse the given string according to the specified format type. + * + * @param string $string + * @param int|null $type + * @param string|null $locale + * @return int|float|false + */ + public static function parse(string $string, ?int $type = NumberFormatter::TYPE_DOUBLE, ?string $locale = null): int|float + { + static::ensureIntlExtensionIsInstalled(); + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::DECIMAL); + + return $formatter->parse($string, $type); + } + + /** + * Parse a string into an integer according to the specified locale. + * + * @param string $string + * @param string|null $locale + * @return int|false + */ + public static function parseInt(string $string, ?string $locale = null): int + { + return self::parse($string, NumberFormatter::TYPE_INT32, $locale); + } + + /** + * Parse a string into a float according to the specified locale. + * + * @param string $string The string to parse + * @param string|null $locale The locale to use + * @return float|false + */ + public static function parseFloat(string $string, ?string $locale = null): float + { + return self::parse($string, NumberFormatter::TYPE_DOUBLE, $locale); + } + /** * Spell out the given number in the given locale. * diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 3bb47011d654..e2909a8d50f9 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -25,6 +25,13 @@ class Str { use Macroable; + /** + * The list of characters that are considered "invisible" in strings. + * + * @var string + */ + const INVISIBLE_CHARACTERS = '\x{0009}\x{0020}\x{00A0}\x{00AD}\x{034F}\x{061C}\x{115F}\x{1160}\x{17B4}\x{17B5}\x{180E}\x{2000}\x{2001}\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}\x{200A}\x{200B}\x{200C}\x{200D}\x{200E}\x{200F}\x{202F}\x{205F}\x{2060}\x{2061}\x{2062}\x{2063}\x{2064}\x{2065}\x{206A}\x{206B}\x{206C}\x{206D}\x{206E}\x{206F}\x{3000}\x{2800}\x{3164}\x{FEFF}\x{FFA0}\x{1D159}\x{1D173}\x{1D174}\x{1D175}\x{1D176}\x{1D177}\x{1D178}\x{1D179}\x{1D17A}\x{E0020}'; + /** * The cache of snake-cased words. * @@ -1180,7 +1187,7 @@ public static function repeat(string $string, int $times) public static function replaceArray($search, $replace, $subject) { if ($replace instanceof Traversable) { - $replace = (new Collection($replace))->all(); + $replace = Arr::from($replace); } $segments = explode($search, $subject); @@ -1222,15 +1229,15 @@ private static function toStringOr($value, $fallback) public static function replace($search, $replace, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = (new Collection($search))->all(); + $search = Arr::from($search); } if ($replace instanceof Traversable) { - $replace = (new Collection($replace))->all(); + $replace = Arr::from($replace); } if ($subject instanceof Traversable) { - $subject = (new Collection($subject))->all(); + $subject = Arr::from($subject); } return $caseSensitive @@ -1363,7 +1370,7 @@ public static function replaceMatches($pattern, $replace, $subject, $limit = -1) public static function remove($search, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = (new Collection($search))->all(); + $search = Arr::from($search); } return $caseSensitive @@ -1569,7 +1576,7 @@ public static function trim($value, $charlist = null) if ($charlist === null) { $trimDefaultCharacters = " \n\r\t\v\0"; - return preg_replace('~^[\s\x{FEFF}\x{200B}\x{200E}'.$trimDefaultCharacters.']+|[\s\x{FEFF}\x{200B}\x{200E}'.$trimDefaultCharacters.']+$~u', '', $value) ?? trim($value); + return preg_replace('~^[\s'.self::INVISIBLE_CHARACTERS.$trimDefaultCharacters.']+|[\s'.self::INVISIBLE_CHARACTERS.$trimDefaultCharacters.']+$~u', '', $value) ?? trim($value); } return trim($value, $charlist); @@ -1587,7 +1594,7 @@ public static function ltrim($value, $charlist = null) if ($charlist === null) { $ltrimDefaultCharacters = " \n\r\t\v\0"; - return preg_replace('~^[\s\x{FEFF}\x{200B}\x{200E}'.$ltrimDefaultCharacters.']+~u', '', $value) ?? ltrim($value); + return preg_replace('~^[\s'.self::INVISIBLE_CHARACTERS.$ltrimDefaultCharacters.']+~u', '', $value) ?? ltrim($value); } return ltrim($value, $charlist); @@ -1605,7 +1612,7 @@ public static function rtrim($value, $charlist = null) if ($charlist === null) { $rtrimDefaultCharacters = " \n\r\t\v\0"; - return preg_replace('~[\s\x{FEFF}\x{200B}\x{200E}'.$rtrimDefaultCharacters.']+$~u', '', $value) ?? rtrim($value); + return preg_replace('~[\s'.self::INVISIBLE_CHARACTERS.$rtrimDefaultCharacters.']+$~u', '', $value) ?? rtrim($value); } return rtrim($value, $charlist); diff --git a/src/Illuminate/Support/Traits/InteractsWithData.php b/src/Illuminate/Support/Traits/InteractsWithData.php index bd39205d942d..5647570eb577 100644 --- a/src/Illuminate/Support/Traits/InteractsWithData.php +++ b/src/Illuminate/Support/Traits/InteractsWithData.php @@ -312,15 +312,16 @@ public function date($key, $format = null, $tz = null) * * @param string $key * @param class-string $enumClass + * @param TEnum|null $default * @return TEnum|null */ - public function enum($key, $enumClass) + public function enum($key, $enumClass, $default = null) { if ($this->isNotFilled($key) || ! $this->isBackedEnum($enumClass)) { - return null; + return value($default); } - return $enumClass::tryFrom($this->data($key)); + return $enumClass::tryFrom($this->data($key)) ?: value($default); } /** diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 4b53abef7e57..404eff091464 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -49,7 +49,7 @@ "suggest": { "illuminate/filesystem": "Required to use the Composer class (^12.0).", "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", - "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.6).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", "league/uri": "Required to use the Uri class (^7.5.1).", "ramsey/uuid": "Required to use Str::uuid() (^4.7).", "symfony/process": "Required to use the Composer class (^7.2).", diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 3f6f59a36730..72f37339238c 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -131,6 +131,21 @@ public function assertSuccessfulPrecognition() return $this; } + /** + * Assert that the response is a client error. + * + * @return $this + */ + public function assertClientError() + { + PHPUnit::withResponse($this)->assertTrue( + $this->isClientError(), + $this->statusMessageWithDetails('>=400, < 500', $this->getStatusCode()) + ); + + return $this; + } + /** * Assert that the response is a server error. * @@ -213,6 +228,23 @@ public function assertRedirectContains($uri) return $this; } + /** + * Assert whether the response is redirecting back to the previous location. + * + * @return $this + */ + public function assertRedirectBack() + { + PHPUnit::withResponse($this)->assertTrue( + $this->isRedirect(), + $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), + ); + + $this->assertLocation(app('url')->previous()); + + return $this; + } + /** * Assert whether the response is redirecting to a given route. * @@ -889,7 +921,7 @@ public function assertJsonMissingPath(string $path) * @param array|null $responseData * @return $this */ - public function assertJsonStructure(?array $structure = null, $responseData = null) + public function assertJsonStructure(?array $structure = null, ?array $responseData = null) { $this->decodeResponseJson()->assertStructure($structure, $responseData); @@ -903,7 +935,7 @@ public function assertJsonStructure(?array $structure = null, $responseData = nu * @param array|null $responseData * @return $this */ - public function assertExactJsonStructure(?array $structure = null, $responseData = null) + public function assertExactJsonStructure(?array $structure = null, ?array $responseData = null) { $this->decodeResponseJson()->assertStructure($structure, $responseData, true); diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 883eb16c4582..0e01f3cb1722 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -232,11 +232,15 @@ protected function appendFilePath($contents) */ protected function getOpenAndClosingPhpTokens($contents) { - return (new Collection(token_get_all($contents))) - ->pluck(0) - ->filter(function ($token) { - return in_array($token, [T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG]); - }); + $tokens = []; + + foreach (token_get_all($contents) as $token) { + if ($token[0] === T_OPEN_TAG || $token[0] === T_OPEN_TAG_WITH_ECHO || $token[0] === T_CLOSE_TAG) { + $tokens[] = $token[0]; + } + } + + return new Collection($tokens); } /** @@ -1013,6 +1017,28 @@ public function precompiler(callable $precompiler) $this->precompilers[] = $precompiler; } + /** + * Execute the given callback using a custom echo format. + * + * @param string $format + * @param callable $callback + * @return string + */ + public function usingEchoFormat($format, callable $callback) + { + $originalEchoFormat = $this->echoFormat; + + $this->setEchoFormat($format); + + try { + $output = call_user_func($callback); + } finally { + $this->setEchoFormat($originalEchoFormat); + } + + return $output; + } + /** * Set the echo format to be used by the compiler. * diff --git a/tests/Console/CommandMutexTest.php b/tests/Console/CommandMutexTest.php index 0743e6f43e7c..528af2fc2057 100644 --- a/tests/Console/CommandMutexTest.php +++ b/tests/Console/CommandMutexTest.php @@ -7,12 +7,15 @@ use Illuminate\Contracts\Console\Isolatable; use Illuminate\Foundation\Application; use Mockery as m; +use Orchestra\Testbench\Concerns\InteractsWithMockery; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; class CommandMutexTest extends TestCase { + use InteractsWithMockery; + /** * @var Command */ @@ -23,6 +26,8 @@ class CommandMutexTest extends TestCase */ protected $commandMutex; + /** {@inheritdoc} */ + #[\Override] protected function setUp(): void { $this->command = new class extends Command implements Isolatable @@ -42,6 +47,13 @@ public function __invoke() $this->command->setLaravel($app); } + /** {@inheritdoc} */ + #[\Override] + protected function tearDown(): void + { + $this->tearDownTheTestEnvironmentUsingMockery(); + } + public function testCanRunIsolatedCommandIfNotBlocked() { $this->commandMutex->shouldReceive('create') diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 5522b8af9b2f..0302729bc970 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -2,9 +2,11 @@ namespace Illuminate\Tests\Container; +use Attribute; use Illuminate\Container\Container; use Illuminate\Container\EntryNotFoundException; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Container\ContextualAttribute; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use stdClass; @@ -522,6 +524,23 @@ public function testGetAlias() $this->assertSame('ConcreteStub', $container->getAlias('foo')); } + public function testCurrentlyResolving() + { + $container = new Container; + + $container->afterResolvingAttribute(ContainerCurrentResolvingAttribute::class, function ($attr, $instance, $container) { + $this->assertEquals(ContainerCurrentResolvingConcrete::class, $container->currentlyResolving()); + }); + + $container->when(ContainerCurrentResolvingConcrete::class) + ->needs('$currentlyResolving') + ->give(fn ($container) => $container->currentlyResolving()); + + $resolved = $container->make(ContainerCurrentResolvingConcrete::class); + + $this->assertEquals(ContainerCurrentResolvingConcrete::class, $resolved->currentlyResolving); + } + public function testGetAliasRecursive() { $container = new Container; @@ -860,3 +879,23 @@ public function work(IContainerContractStub $stub) return $stub; } } + +#[Attribute(Attribute::TARGET_PARAMETER)] +class ContainerCurrentResolvingAttribute implements ContextualAttribute +{ + public function resolve() + { + } +} + +class ContainerCurrentResolvingConcrete +{ + public $currentlyResolving; + + public function __construct( + #[ContainerCurrentResolvingAttribute] + string $currentlyResolving + ) { + $this->currentlyResolving = $currentlyResolving; + } +} diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 96a6beed7e20..09146044a349 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1282,6 +1282,7 @@ public function testWhereAttachedTo() { $related = new EloquentBuilderTestModelFarRelatedStub; $related->id = 49; + $related->name = 'test'; $builder = EloquentBuilderTestModelParentStub::whereAttachedTo($related, 'roles'); @@ -1292,9 +1293,11 @@ public function testWhereAttachedToCollection() { $model1 = new EloquentBuilderTestModelParentStub; $model1->id = 3; + $model1->name = 'test3'; $model2 = new EloquentBuilderTestModelParentStub; $model2->id = 4; + $model2->name = 'test4'; $builder = EloquentBuilderTestModelFarRelatedStub::whereAttachedTo(new Collection([$model1, $model2]), 'roles'); diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 1d176afa00f1..c27cbfe476b1 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -173,6 +173,7 @@ protected function createSchema() $this->schema($connection)->create('achievements', function ($table) { $table->increments('id'); + $table->integer('status')->nullable(); }); $this->schema($connection)->create('eloquent_test_achievement_eloquent_test_user', function ($table) { @@ -1497,7 +1498,7 @@ public function testWhereAttachedTo() $user1 = EloquentTestUser::create(['email' => 'user1@gmail.com']); $user2 = EloquentTestUser::create(['email' => 'user2@gmail.com']); $user3 = EloquentTestUser::create(['email' => 'user3@gmail.com']); - $achievement1 = EloquentTestAchievement::create(); + $achievement1 = EloquentTestAchievement::create(['status' => 3]); $achievement2 = EloquentTestAchievement::create(); $achievement3 = EloquentTestAchievement::create(); @@ -2988,6 +2989,7 @@ class EloquentTestAchievement extends Eloquent public $timestamps = false; protected $table = 'achievements'; + protected $guarded = []; public function eloquentTestUsers() { diff --git a/tests/Database/DatabaseMariaDbSchemaGrammarTest.php b/tests/Database/DatabaseMariaDbSchemaGrammarTest.php index 1224247e20de..dff05674d9ff 100755 --- a/tests/Database/DatabaseMariaDbSchemaGrammarTest.php +++ b/tests/Database/DatabaseMariaDbSchemaGrammarTest.php @@ -873,7 +873,11 @@ public function testAddingJsonb() public function testAddingDate() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->date('foo'); $statements = $blueprint->toSql(); @@ -881,15 +885,47 @@ public function testAddingDate() $this->assertSame('alter table `users` add `foo` date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `foo` date not null default (CURDATE())', $statements[0]); + } + public function testAddingYear() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->year('birth_year'); $statements = $blueprint->toSql(); $this->assertCount(1, $statements); $this->assertSame('alter table `users` add `birth_year` year not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `birth_year` year not null default (YEAR(CURDATE()))', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index 09082dab62df..cfc6bb02df3b 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -872,7 +872,11 @@ public function testAddingJsonb() public function testAddingDate() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->date('foo'); $statements = $blueprint->toSql(); @@ -880,15 +884,75 @@ public function testAddingDate() $this->assertSame('alter table `users` add `foo` date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `foo` date not null default (CURDATE())', $statements[0]); + } + + public function testAddingDateWithDefaultCurrentOn57() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('5.7'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `foo` date not null', $statements[0]); + } + public function testAddingYear() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->year('birth_year'); $statements = $blueprint->toSql(); $this->assertCount(1, $statements); $this->assertSame('alter table `users` add `birth_year` year not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `birth_year` year not null default (YEAR(CURDATE()))', $statements[0]); + } + + public function testAddingYearWithDefaultCurrentOn57() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('5.7'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `birth_year` year not null', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseMySqlSchemaStateTest.php b/tests/Database/DatabaseMySqlSchemaStateTest.php index 3a9c7db3896c..18985114e509 100644 --- a/tests/Database/DatabaseMySqlSchemaStateTest.php +++ b/tests/Database/DatabaseMySqlSchemaStateTest.php @@ -70,6 +70,24 @@ public static function provider(): Generator ], ]; + yield 'no_ssl' => [ + ' --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --ssl=off', [ + 'LARAVEL_LOAD_SOCKET' => '', + 'LARAVEL_LOAD_HOST' => '', + 'LARAVEL_LOAD_PORT' => '', + 'LARAVEL_LOAD_USER' => 'root', + 'LARAVEL_LOAD_PASSWORD' => '', + 'LARAVEL_LOAD_DATABASE' => 'forge', + 'LARAVEL_LOAD_SSL_CA' => '', + ], [ + 'username' => 'root', + 'database' => 'forge', + 'options' => [ + \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, + ], + ], + ]; + yield 'unix socket' => [ ' --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --socket="${:LARAVEL_LOAD_SOCKET}"', [ 'LARAVEL_LOAD_SOCKET' => '/tmp/mysql.sock', diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 0c31a6d2d06a..f3402250245f 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -714,6 +714,16 @@ public function testAddingDate() $this->assertSame('alter table "users" add column "foo" date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" date not null default CURRENT_DATE', $statements[0]); + } + public function testAddingYear() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -723,6 +733,15 @@ public function testAddingYear() $this->assertSame('alter table "users" add column "birth_year" integer not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "birth_year" integer not null default EXTRACT(YEAR FROM CURRENT_DATE)', $statements[0]); + } + public function testAddingJson() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index d9233f548fa9..26de4bcf0bf8 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -640,6 +640,16 @@ public function testAddingDate() $this->assertSame('alter table "users" add column "foo" date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" date not null default CURRENT_DATE', $statements[0]); + } + public function testAddingYear() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -649,6 +659,15 @@ public function testAddingYear() $this->assertSame('alter table "users" add column "birth_year" integer not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "birth_year" integer not null default (CAST(strftime(\'%Y\', \'now\') AS INTEGER))', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index e8546270597b..e5f26379caa6 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -102,6 +102,31 @@ public function testDropIndexDefaultNamesWhenPrefixSupplied() $this->assertSame('prefix_geo_coordinates_spatialindex', $commands[0]->index); } + public function testDefaultCurrentDate() + { + $getSql = function ($grammar, $mysql57 = false) { + if ($grammar == 'MySql') { + $connection = $this->getConnection($grammar); + $mysql57 ? $connection->shouldReceive('getServerVersion')->andReturn('5.7') : $connection->shouldReceive('getServerVersion')->andReturn('8.0.13'); + $connection->shouldReceive('isMaria')->andReturn(false); + + return (new Blueprint($connection, 'users', function ($table) { + $table->date('created')->useCurrent(); + }))->toSql(); + } else { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->date('created')->useCurrent(); + })->toSql(); + } + }; + + $this->assertEquals(['alter table `users` add `created` date not null default (CURDATE())'], $getSql('MySql')); + $this->assertEquals(['alter table `users` add `created` date not null'], $getSql('MySql', mysql57: true)); + $this->assertEquals(['alter table "users" add column "created" date not null default CURRENT_DATE'], $getSql('Postgres')); + $this->assertEquals(['alter table "users" add column "created" date not null default CURRENT_DATE'], $getSql('SQLite')); + $this->assertEquals(['alter table "users" add "created" date not null default CAST(GETDATE() AS DATE)'], $getSql('SqlServer')); + } + public function testDefaultCurrentDateTime() { $getSql = function ($grammar) { @@ -130,6 +155,31 @@ public function testDefaultCurrentTimestamp() $this->assertEquals(['alter table "users" add "created" datetime not null default CURRENT_TIMESTAMP'], $getSql('SqlServer')); } + public function testDefaultCurrentYear() + { + $getSql = function ($grammar, $mysql57 = false) { + if ($grammar == 'MySql') { + $connection = $this->getConnection($grammar); + $mysql57 ? $connection->shouldReceive('getServerVersion')->andReturn('5.7') : $connection->shouldReceive('getServerVersion')->andReturn('8.0.13'); + $connection->shouldReceive('isMaria')->andReturn(false); + + return (new Blueprint($connection, 'users', function ($table) { + $table->year('birth_year')->useCurrent(); + }))->toSql(); + } else { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->year('birth_year')->useCurrent(); + })->toSql(); + } + }; + + $this->assertEquals(['alter table `users` add `birth_year` year not null default (YEAR(CURDATE()))'], $getSql('MySql')); + $this->assertEquals(['alter table `users` add `birth_year` year not null'], $getSql('MySql', mysql57: true)); + $this->assertEquals(['alter table "users" add column "birth_year" integer not null default EXTRACT(YEAR FROM CURRENT_DATE)'], $getSql('Postgres')); + $this->assertEquals(['alter table "users" add column "birth_year" integer not null default (CAST(strftime(\'%Y\', \'now\') AS INTEGER))'], $getSql('SQLite')); + $this->assertEquals(['alter table "users" add "birth_year" int not null default CAST(YEAR(GETDATE()) AS INTEGER)'], $getSql('SqlServer')); + } + public function testRemoveColumn() { $getSql = function ($grammar) { diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index d06003473498..c31cf83f5576 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -614,6 +614,16 @@ public function testAddingDate() $this->assertSame('alter table "users" add "foo" date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add "foo" date not null default CAST(GETDATE() AS DATE)', $statements[0]); + } + public function testAddingYear() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -623,6 +633,15 @@ public function testAddingYear() $this->assertSame('alter table "users" add "birth_year" int not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add "birth_year" int not null default CAST(YEAR(GETDATE()) AS INTEGER)', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index 9724a36bdbd2..4f9ec170ba1f 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -199,6 +199,23 @@ public function testQueuePropagateMiddleware() && $job->middleware[0]->b === 'bar'; }); } + + public function testDispatchesOnQueueDefinedWithEnum() + { + $d = new Dispatcher; + $queue = m::mock(Queue::class); + + $fakeQueue = new QueueFake(new Container); + + $d->setQueueResolver(function () use ($fakeQueue) { + return $fakeQueue; + }); + + $d->listen('some.event', TestDispatcherViaQueueSupportsEnum::class.'@handle'); + $d->dispatch('some.event', ['foo', 'bar']); + + $fakeQueue->assertPushedOn('enumerated-queue', CallQueuedListener::class); + } } class TestDispatcherQueuedHandler implements ShouldQueue @@ -367,3 +384,16 @@ public function withDelay($event) return 20; } } + +enum TestQueueType: string +{ + case EnumeratedQueue = 'enumerated-queue'; +} + +class TestDispatcherViaQueueSupportsEnum implements ShouldQueue +{ + public function viaQueue() + { + return TestQueueType::EnumeratedQueue; + } +} diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 56ee25932b0f..af70017090ef 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -21,6 +21,7 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\Response; use Illuminate\Http\Client\ResponseSequence; +use Illuminate\Http\Client\StrayRequestException; use Illuminate\Http\Response as HttpResponse; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; @@ -3178,12 +3179,38 @@ public function testItCanEnforceFaking() $responses[] = $this->factory->get('https://forge.laravel.com')->body(); $this->assertSame(['ok', 'ok'], $responses); - $this->expectException(RuntimeException::class); + $this->expectException(StrayRequestException::class); $this->expectExceptionMessage('Attempted request to [https://laravel.com] without a matching fake.'); $this->factory->get('https://laravel.com'); } + public function testItCanEnforceFakingInThePool() + { + $this->factory->preventStrayRequests(); + $this->factory->fake(['https://vapor.laravel.com' => Factory::response('ok', 200)]); + $this->factory->fake(['https://forge.laravel.com' => Factory::response('ok', 200)]); + + $responses = $this->factory->pool(function (Pool $pool) { + return [ + $pool->get('https://vapor.laravel.com'), + $pool->get('https://forge.laravel.com'), + ]; + }); + + $this->assertSame(200, $responses[0]->status()); + $this->assertSame(200, $responses[1]->status()); + + $this->expectException(StrayRequestException::class); + $this->expectExceptionMessage('Attempted request to [https://laravel.com] without a matching fake.'); + + $this->factory->pool(function (Pool $pool) { + return [ + $pool->get('https://laravel.com'), + ]; + }); + } + public function testPreventingStrayRequests() { $this->assertFalse($this->factory->preventingStrayRequests()); diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 249b3043691e..08ab80bc6d80 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -804,6 +804,9 @@ public function testEnumMethod() $this->assertNull($request->enum('doesnt_exist', TestEnumBacked::class)); + $this->assertEquals(TestEnumBacked::test, $request->enum('invalid_enum_value', TestEnumBacked::class, TestEnumBacked::test)); + $this->assertEquals(TestEnumBacked::test, $request->enum('missing_key', TestEnumBacked::class, TestEnumBacked::test)); + $this->assertEquals(TestEnumBacked::test, $request->enum('valid_enum_value', TestEnumBacked::class)); $this->assertNull($request->enum('invalid_enum_value', TestEnumBacked::class)); diff --git a/tests/Http/Middleware/VitePreloadingTest.php b/tests/Http/Middleware/VitePreloadingTest.php index 4fb11f8b0b37..765ee029590a 100644 --- a/tests/Http/Middleware/VitePreloadingTest.php +++ b/tests/Http/Middleware/VitePreloadingTest.php @@ -106,4 +106,47 @@ public function testItDoesNotOverwriteOtherLinkHeaders() $response->headers->all('Link'), ); } + + public function testItCanLimitNumberOfAssetsPreloaded() + { + $app = new Container; + $app->instance(Vite::class, new class extends Vite + { + protected $preloadedAssets = [ + 'https://laravel.com/first.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + 'https://laravel.com/second.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + 'https://laravel.com/third.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + 'https://laravel.com/fourth.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + ]; + }); + Facade::setFacadeApplication($app); + + $response = (new AddLinkHeadersForPreloadedAssets)->handle(new Request, fn () => new Response('ok'), 2); + + $this->assertSame( + [ + '; rel="modulepreload"; foo="bar", ; rel="modulepreload"; foo="bar"', + ], + $response->headers->all('Link'), + ); + } + + public function test_it_can_configure_the_middleware() + { + $definition = AddLinkHeadersForPreloadedAssets::using(limit: 5); + + $this->assertSame('Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets:5', $definition); + } } diff --git a/tests/Integration/Cache/FileCacheLockTest.php b/tests/Integration/Cache/FileCacheLockTest.php index c9eff5ced0e9..575f1220b852 100644 --- a/tests/Integration/Cache/FileCacheLockTest.php +++ b/tests/Integration/Cache/FileCacheLockTest.php @@ -3,17 +3,25 @@ namespace Illuminate\Tests\Integration\Cache; use Exception; +use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Sleep; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; #[WithConfig('cache.default', 'file')] class FileCacheLockTest extends TestCase { - public function testLocksCanBeAcquiredAndReleased() + protected function setUp(): void { + parent::setUp(); + + // flush lock from previous tests Cache::lock('foo')->forceRelease(); + } + public function testLocksCanBeAcquiredAndReleased() + { $lock = Cache::lock('foo', 10); $this->assertTrue($lock->get()); $this->assertFalse(Cache::lock('foo', 10)->get()); @@ -27,7 +35,6 @@ public function testLocksCanBeAcquiredAndReleased() public function testLocksCanBlockForSeconds() { - Cache::lock('foo')->forceRelease(); $this->assertSame('taylor', Cache::lock('foo', 10)->block(1, function () { return 'taylor'; })); @@ -38,11 +45,11 @@ public function testLocksCanBlockForSeconds() public function testConcurrentLocksAreReleasedSafely() { - Cache::lock('foo')->forceRelease(); + Sleep::fake(syncWithCarbon: true); $firstLock = Cache::lock('foo', 1); $this->assertTrue($firstLock->get()); - sleep(2); + Sleep::for(2)->seconds(); $secondLock = Cache::lock('foo', 10); $this->assertTrue($secondLock->get()); @@ -54,8 +61,6 @@ public function testConcurrentLocksAreReleasedSafely() public function testLocksWithFailedBlockCallbackAreReleased() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); try { @@ -75,8 +80,6 @@ public function testLocksWithFailedBlockCallbackAreReleased() public function testLocksCanBeReleasedUsingOwnerToken() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); $this->assertTrue($firstLock->get()); $owner = $firstLock->owner(); @@ -89,8 +92,6 @@ public function testLocksCanBeReleasedUsingOwnerToken() public function testOwnerStatusCanBeCheckedAfterRestoringLock() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); $this->assertTrue($firstLock->get()); $owner = $firstLock->owner(); @@ -101,8 +102,6 @@ public function testOwnerStatusCanBeCheckedAfterRestoringLock() public function testOtherOwnerDoesNotOwnLockAfterRestore() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); $this->assertTrue($firstLock->isOwnedBy(null)); $this->assertTrue($firstLock->get()); @@ -112,4 +111,16 @@ public function testOtherOwnerDoesNotOwnLockAfterRestore() $this->assertTrue($secondLock->isOwnedBy($firstLock->owner())); $this->assertFalse($secondLock->isOwnedByCurrentProcess()); } + + public function testExceptionIfBlockCanNotAcquireLock() + { + Sleep::fake(syncWithCarbon: true); + + // acquire and not release lock + Cache::lock('foo', 10)->get(); + + // try to get lock and hit block timeout + $this->expectException(LockTimeoutException::class); + Cache::lock('foo', 10)->block(5); + } } diff --git a/tests/Integration/Cache/MemoizedStoreTest.php b/tests/Integration/Cache/MemoizedStoreTest.php index 028688b262e2..009906f1555f 100644 --- a/tests/Integration/Cache/MemoizedStoreTest.php +++ b/tests/Integration/Cache/MemoizedStoreTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Cache; +use BadMethodCallException; use Illuminate\Cache\Events\CacheEvent; use Illuminate\Cache\Events\CacheMissed; use Illuminate\Cache\Events\ForgettingKey; @@ -10,12 +11,15 @@ use Illuminate\Cache\Events\RetrievingKey; use Illuminate\Cache\Events\RetrievingManyKeys; use Illuminate\Cache\Events\WritingKey; +use Illuminate\Contracts\Cache\Store; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Exceptions; use Illuminate\Support\Facades\Redis; use Orchestra\Testbench\TestCase; +use Throwable; class MemoizedStoreTest extends TestCase { @@ -70,7 +74,7 @@ public function test_null_values_are_memoized_when_retrieving_single_value() $this->assertNull($memoized); } - public function test_it_can_memoize_when_retrieving_mulitple_values() + public function test_it_can_memoize_when_retrieving_multiple_values() { Cache::put('name.0', 'Tim', 60); Cache::put('name.1', 'Taylor', 60); @@ -116,7 +120,7 @@ public function test_it_uses_correct_keys_for_getMultiple() $this->assertSame($cacheValue, $memoValue); } - public function test_null_values_are_memoized_when_retrieving_mulitple_values() + public function test_null_values_are_memoized_when_retrieving_multiple_values() { $live = Cache::getMultiple(['name.0', 'name.1']); $memoized = Cache::memo()->getMultiple(['name.0', 'name.1']); @@ -132,7 +136,7 @@ public function test_null_values_are_memoized_when_retrieving_mulitple_values() $this->assertSame($memoized, ['name.0' => null, 'name.1' => null]); } - public function test_it_can_retrieve_already_memoized_and_not_yet_memoized_values_when_retrieving_mulitple_values() + public function test_it_can_retrieve_already_memoized_and_not_yet_memoized_values_when_retrieving_multiple_values() { Cache::put('name.0', 'Tim', 60); Cache::put('name.1', 'Taylor', 60); @@ -406,4 +410,89 @@ public function test_it_resets_cache_store_with_scoped_instances() $this->assertSame('Taylor', $live); $this->assertSame('Taylor', $memoized); } + + public function test_it_throws_when_underlying_store_does_not_support_locks() + { + $this->freezeTime(); + $exceptions = []; + Exceptions::reportable(function (Throwable $e) use (&$exceptions) { + $exceptions[] = $e; + }); + Config::set('cache.stores.no-lock', ['driver' => 'no-lock']); + Cache::extend('no-lock', fn () => Cache::repository(new class implements Store + { + public function get($key) + { + return Cache::get(...func_get_args()); + } + + public function many(array $keys) + { + return Cache::many(...func_get_args()); + } + + public function put($key, $value, $seconds) + { + return Cache::put(...func_get_args()); + } + + public function putMany(array $values, $seconds) + { + return Cache::putMany(...func_get_args()); + } + + public function increment($key, $value = 1) + { + return Cache::increment(...func_get_args()); + } + + public function decrement($key, $value = 1) + { + return Cache::decrement(...func_get_args()); + } + + public function forever($key, $value) + { + return Cache::forever(...func_get_args()); + } + + public function forget($key) + { + return Cache::forget(...func_get_args()); + } + + public function flush() + { + return Cache::flush(...func_get_args()); + } + + public function getPrefix() + { + return Cache::getPrefix(...func_get_args()); + } + })); + Cache::flexible('key', [10, 20], 'value-1'); + + $this->travel(11)->seconds(); + Cache::memo('no-lock')->flexible('key', [10, 20], 'value-2'); + defer()->invoke(); + $value = Cache::get('key'); + + $this->assertCount(1, $exceptions); + $this->assertInstanceOf(BadMethodCallException::class, $exceptions[0]); + $this->assertSame('This cache store does not support locks.', $exceptions[0]->getMessage()); + } + + public function test_it_supports_with_flexible() + { + $this->freezeTime(); + Cache::flexible('key', [10, 20], 'value-1'); + + $this->travel(11)->seconds(); + Cache::memo()->flexible('key', [10, 20], 'value-2'); + defer()->invoke(); + $value = Cache::get('key'); + + $this->assertSame('value-2', $value); + } } diff --git a/tests/Integration/Database/EloquentModelRefreshTest.php b/tests/Integration/Database/EloquentModelRefreshTest.php index 4bac91dbfe32..304c04f10883 100644 --- a/tests/Integration/Database/EloquentModelRefreshTest.php +++ b/tests/Integration/Database/EloquentModelRefreshTest.php @@ -54,6 +54,18 @@ public function testItSyncsOriginalOnRefresh() $this->assertSame('patrick', $post->getOriginal('title')); } + public function testItDoesNotSyncPreviousOnRefresh() + { + $post = Post::create(['title' => 'pat']); + + Post::find($post->id)->update(['title' => 'patrick']); + + $post->refresh(); + + $this->assertEmpty($post->getDirty()); + $this->assertEmpty($post->getPrevious()); + } + public function testAsPivot() { Schema::create('post_posts', function (Blueprint $table) { diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index d4ad6c207437..80bc917e250c 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -42,25 +42,28 @@ public function testUserCanUpdateNullableDate() public function testAttributeChanges() { $user = TestModel2::create([ - 'name' => Str::random(), 'title' => Str::random(), + 'name' => $originalName = Str::random(), 'title' => Str::random(), ]); $this->assertEmpty($user->getDirty()); $this->assertEmpty($user->getChanges()); + $this->assertEmpty($user->getPrevious()); $this->assertFalse($user->isDirty()); $this->assertFalse($user->wasChanged()); - $user->name = $name = Str::random(); + $user->name = $overrideName = Str::random(); - $this->assertEquals(['name' => $name], $user->getDirty()); + $this->assertEquals(['name' => $overrideName], $user->getDirty()); $this->assertEmpty($user->getChanges()); + $this->assertEmpty($user->getPrevious()); $this->assertTrue($user->isDirty()); $this->assertFalse($user->wasChanged()); $user->save(); $this->assertEmpty($user->getDirty()); - $this->assertEquals(['name' => $name], $user->getChanges()); + $this->assertEquals(['name' => $overrideName], $user->getChanges()); + $this->assertEquals(['name' => $originalName], $user->getPrevious()); $this->assertTrue($user->wasChanged()); $this->assertTrue($user->wasChanged('name')); } @@ -73,6 +76,7 @@ public function testDiscardChanges() $this->assertEmpty($user->getDirty()); $this->assertEmpty($user->getChanges()); + $this->assertEmpty($user->getPrevious()); $this->assertFalse($user->isDirty()); $this->assertFalse($user->wasChanged()); @@ -80,6 +84,7 @@ public function testDiscardChanges() $this->assertEquals(['name' => $overrideName], $user->getDirty()); $this->assertEmpty($user->getChanges()); + $this->assertEmpty($user->getPrevious()); $this->assertTrue($user->isDirty()); $this->assertFalse($user->wasChanged()); $this->assertSame($originalName, $user->getOriginal('name')); @@ -88,11 +93,15 @@ public function testDiscardChanges() $user->discardChanges(); $this->assertEmpty($user->getDirty()); + $this->assertEmpty($user->getChanges()); + $this->assertEmpty($user->getPrevious()); $this->assertSame($originalName, $user->getOriginal('name')); $this->assertSame($originalName, $user->getAttribute('name')); $user->save(); $this->assertFalse($user->wasChanged()); + $this->assertEmpty($user->getChanges()); + $this->assertEmpty($user->getPrevious()); } public function testInsertRecordWithReservedWordFieldName() diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index e94fe5cce005..8521b948e8e9 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -179,6 +179,16 @@ public function testCustomMorphPivotClassDetachAttributes() $project->equipments()->save($equipment); $equipment->projects()->sync([]); + + $this->assertEquals( + [PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class], + PivotEventsTestModelEquipment::$eventsMorphClasses + ); + + $this->assertEquals( + ['equipmentable_type', 'equipmentable_type', 'equipmentable_type', 'equipmentable_type', 'equipmentable_type', 'equipmentable_type'], + PivotEventsTestModelEquipment::$eventsMorphTypes + ); } } @@ -237,6 +247,55 @@ class PivotEventsTestModelEquipment extends MorphPivot { public $table = 'equipmentables'; + public static $eventsMorphClasses = []; + + public static $eventsMorphTypes = []; + + public static function boot() + { + parent::boot(); + + static::creating(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::created(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::updating(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::updated(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::saving(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::saved(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::deleting(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::deleted(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + } + public function equipment() { return $this->belongsTo(PivotEventsTestEquipment::class); diff --git a/tests/Integration/Database/EloquentUpdateTest.php b/tests/Integration/Database/EloquentUpdateTest.php index 68fdc26993a2..829091686abf 100644 --- a/tests/Integration/Database/EloquentUpdateTest.php +++ b/tests/Integration/Database/EloquentUpdateTest.php @@ -136,6 +136,50 @@ public function testIncrementOrDecrementIgnoresGlobalScopes() $deletedModel->decrement('counter'); $this->assertEquals(0, $deletedModel->fresh()->counter); } + + public function testUpdateSyncsPrevious() + { + $model = TestUpdateModel1::create([ + 'name' => Str::random(), + 'title' => 'Ms.', + ]); + + $model->update(['title' => 'Dr.']); + + $this->assertSame('Dr.', $model->title); + $this->assertSame('Dr.', $model->getOriginal('title')); + $this->assertSame(['title' => 'Dr.'], $model->getChanges()); + $this->assertSame(['title' => 'Ms.'], $model->getPrevious()); + } + + public function testSaveSyncsPrevious() + { + $model = TestUpdateModel1::create([ + 'name' => Str::random(), + 'title' => 'Ms.', + ]); + + $model->title = 'Dr.'; + $model->save(); + + $this->assertSame('Dr.', $model->title); + $this->assertSame('Dr.', $model->getOriginal('title')); + $this->assertSame(['title' => 'Dr.'], $model->getChanges()); + $this->assertSame(['title' => 'Ms.'], $model->getPrevious()); + } + + public function testIncrementSyncsPrevious() + { + $model = TestUpdateModel3::create([ + 'counter' => 0, + ]); + + $model->increment('counter'); + + $this->assertEquals(1, $model->counter); + $this->assertSame(['counter' => 1], $model->getChanges()); + $this->assertSame(['counter' => 0], $model->getPrevious()); + } } class TestUpdateModel1 extends Model diff --git a/tests/Integration/Foundation/Console/AboutCommandTest.php b/tests/Integration/Foundation/Console/AboutCommandTest.php index 9741d174861a..7eb15a12bd7e 100644 --- a/tests/Integration/Foundation/Console/AboutCommandTest.php +++ b/tests/Integration/Foundation/Console/AboutCommandTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Foundation\Console; use Illuminate\Testing\Assert; +use Orchestra\Testbench\Attributes\WithEnv; use Orchestra\Testbench\TestCase; use function Orchestra\Testbench\remote; @@ -40,4 +41,16 @@ public function testItCanDisplayAboutCommandAsJson() ], $output['drivers']); }); } + + #[WithEnv('VIEW_COMPILED_PATH', __DIR__.'/../../View/templates')] + public function testItRespectsCustomPathForCompiledViews(): void + { + $process = remote('about --json', ['APP_ENV' => 'local'])->mustRun(); + + tap(json_decode($process->getOutput(), true), static function (array $output) { + Assert::assertArraySubset([ + 'views' => true, + ], $output['cache']); + }); + } } diff --git a/tests/Integration/Foundation/DiscoverEventsTest.php b/tests/Integration/Foundation/DiscoverEventsTest.php index 116f2374afba..417358539e24 100644 --- a/tests/Integration/Foundation/DiscoverEventsTest.php +++ b/tests/Integration/Foundation/DiscoverEventsTest.php @@ -57,6 +57,33 @@ class_alias(UnionListener::class, 'Tests\Integration\Foundation\Fixtures\EventDi ], $events); } + public function testMultipleDirectoriesCanBeDiscovered(): void + { + $events = DiscoverEvents::within([ + __DIR__.'/Fixtures/EventDiscovery/Listeners', + __DIR__.'/Fixtures/EventDiscovery/UnionListeners', + ], getcwd()); + + $this->assertEquals([ + EventOne::class => [ + Listener::class.'@handle', + Listener::class.'@handleEventOne', + UnionListener::class.'@handle', + ], + EventTwo::class => [ + Listener::class.'@handleEventTwo', + UnionListener::class.'@handle', + ], + ], $events); + } + + public function testNoExceptionForEmptyDirectories(): void + { + $events = DiscoverEvents::within([], getcwd()); + + $this->assertEquals([], $events); + } + public function testEventsCanBeDiscoveredUsingCustomClassNameGuessing() { DiscoverEvents::guessClassNamesUsing(function (SplFileInfo $file, $basePath) { diff --git a/tests/Integration/Mail/Fixtures/message-with-template.blade.php b/tests/Integration/Mail/Fixtures/message-with-template.blade.php new file mode 100644 index 000000000000..9c53cef7e1bb --- /dev/null +++ b/tests/Integration/Mail/Fixtures/message-with-template.blade.php @@ -0,0 +1,4 @@ +@component('mail::message') +*Hi* {{ $user->name }} + +@endcomponent diff --git a/tests/Integration/Mail/Fixtures/table-with-template.blade.php b/tests/Integration/Mail/Fixtures/table-with-template.blade.php new file mode 100644 index 000000000000..3a4ec4c6260e --- /dev/null +++ b/tests/Integration/Mail/Fixtures/table-with-template.blade.php @@ -0,0 +1,12 @@ + + + +*Hi* {{ $user->name }} + +| Laravel | Table | Example | +| ------------- | :-----------: | ------------: | +| Col 2 is | Centered | $10 | +| Col 3 is | Right-Aligned | $20 | + + + diff --git a/tests/Integration/Mail/MailableTestCase.php b/tests/Integration/Mail/MailableTestCase.php new file mode 100644 index 000000000000..4790652fb20d --- /dev/null +++ b/tests/Integration/Mail/MailableTestCase.php @@ -0,0 +1,72 @@ +addLocation(__DIR__.'/Fixtures'); + } + + #[DataProvider('markdownEncodedDataProvider')] + public function testItCanAssertMarkdownEncodedString($given, $expected) + { + $mailable = new class($given) extends Mailable + { + public function __construct(public string $message) + { + // + } + + public function envelope() + { + return new Envelope( + subject: 'My basic title', + ); + } + + public function content() + { + return new Content( + markdown: 'message', + ); + } + }; + + $mailable->assertSeeInHtml($expected, false); + } + + public static function markdownEncodedDataProvider() + { + yield ['[Laravel](https://laravel.com)', 'My message is: [Laravel](https://laravel.com)']; + + yield [ + '![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', + 'My message is: ![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', + ]; + + yield [ + 'Visit https://laravel.com/docs to browse the documentation', + 'My message is: Visit https://laravel.com/docs to browse the documentation', + ]; + + yield [ + 'Visit to browse the documentation', + 'My message is: Visit <https://laravel.com/docs> to browse the documentation', + ]; + + yield [ + 'Visit https://laravel.com/docs to browse the documentation', + 'My message is: Visit <span>https://laravel.com/docs</span> to browse the documentation', + ]; + } +} diff --git a/tests/Integration/Mail/MailableWithSecuredEncodingTest.php b/tests/Integration/Mail/MailableWithSecuredEncodingTest.php new file mode 100644 index 000000000000..12c95c05262d --- /dev/null +++ b/tests/Integration/Mail/MailableWithSecuredEncodingTest.php @@ -0,0 +1,126 @@ +create([ + 'name' => $given, + ]); + + $mailable = new class($user) extends Mailable + { + public $theme = 'taylor'; + + public function __construct(public User $user) + { + // + } + + public function build() + { + return $this->markdown('message-with-template'); + } + }; + + $mailable->assertSeeInHtml($expected, false); + } + + #[WithMigration] + #[DataProvider('markdownEncodedTemplateDataProvider')] + public function testItCanAssertMarkdownEncodedStringUsingTemplateWithTable($given, $expected) + { + $user = UserFactory::new()->create([ + 'name' => $given, + ]); + + $mailable = new class($user) extends Mailable + { + public $theme = 'taylor'; + + public function __construct(public User $user) + { + // + } + + public function build() + { + return $this->markdown('table-with-template'); + } + }; + + $mailable->assertSeeInHtml($expected, false); + $mailable->assertSeeInHtml('

This is a subcopy

', false); + $mailable->assertSeeInHtml(<<<'TABLE' + + + + + + + + + + + + + + + + + + + + +
LaravelTableExample
Col 2 isCentered$10
Col 3 isRight-Aligned$20
+TABLE, false); + } + + public static function markdownEncodedTemplateDataProvider() + { + yield ['[Laravel](https://laravel.com)', 'Hi [Laravel](https://laravel.com)']; + + yield [ + '![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', + 'Hi ![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', + ]; + + yield [ + 'Visit https://laravel.com/docs to browse the documentation', + 'Hi Visit https://laravel.com/docs to browse the documentation', + ]; + + yield [ + 'Visit to browse the documentation', + 'Hi Visit <https://laravel.com/docs> to browse the documentation', + ]; + + yield [ + 'Visit https://laravel.com/docs to browse the documentation', + 'Hi Visit <span>https://laravel.com/docs</span> to browse the documentation', + ]; + } +} diff --git a/tests/Integration/Mail/MailableWithoutSecuredEncodingTest.php b/tests/Integration/Mail/MailableWithoutSecuredEncodingTest.php new file mode 100644 index 000000000000..5ded70eb246c --- /dev/null +++ b/tests/Integration/Mail/MailableWithoutSecuredEncodingTest.php @@ -0,0 +1,126 @@ +create([ + 'name' => $given, + ]); + + $mailable = new class($user) extends Mailable + { + public $theme = 'taylor'; + + public function __construct(public User $user) + { + // + } + + public function build() + { + return $this->markdown('message-with-template'); + } + }; + + $mailable->assertSeeInHtml($expected, false); + } + + #[WithMigration] + #[DataProvider('markdownEncodedTemplateDataProvider')] + public function testItCanAssertMarkdownEncodedStringUsingTemplateWithTable($given, $expected) + { + $user = UserFactory::new()->create([ + 'name' => $given, + ]); + + $mailable = new class($user) extends Mailable + { + public $theme = 'taylor'; + + public function __construct(public User $user) + { + // + } + + public function build() + { + return $this->markdown('table-with-template'); + } + }; + + $mailable->assertSeeInHtml($expected, false); + $mailable->assertSeeInHtml('

This is a subcopy

', false); + $mailable->assertSeeInHtml(<<<'TABLE' + + + + + + + + + + + + + + + + + + + + +
LaravelTableExample
Col 2 isCentered$10
Col 3 isRight-Aligned$20
+TABLE, false); + } + + public static function markdownEncodedTemplateDataProvider() + { + yield ['[Laravel](https://laravel.com)', '

Hi Laravel

']; + + yield [ + '![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', + '

Hi Welcome to Laravel

', + ]; + + yield [ + 'Visit https://laravel.com/docs to browse the documentation', + 'Hi Visit https://laravel.com/docs to browse the documentation', + ]; + + yield [ + 'Visit to browse the documentation', + 'Hi Visit <https://laravel.com/docs> to browse the documentation', + ]; + + yield [ + 'Visit https://laravel.com/docs to browse the documentation', + 'Hi Visit <span>https://laravel.com/docs</span> to browse the documentation', + ]; + } +} diff --git a/tests/Integration/Mail/MarkdownParserTest.php b/tests/Integration/Mail/MarkdownParserTest.php new file mode 100644 index 000000000000..16910e79fd18 --- /dev/null +++ b/tests/Integration/Mail/MarkdownParserTest.php @@ -0,0 +1,89 @@ +assertInstanceOf(HtmlString::class, $html); + + $this->assertStringEqualsStringIgnoringLineEndings($expected.PHP_EOL, (string) $html); + $this->assertSame((string) $html, (string) $html->toHtml()); + }); + } + + #[DataProvider('markdownEncodedDataProvider')] + public function testItCanParseMarkdownEncodedString($given, $expected) + { + tap(Markdown::parse($given, encoded: true), function ($html) use ($expected) { + $this->assertInstanceOf(HtmlString::class, $html); + + $this->assertStringEqualsStringIgnoringLineEndings($expected.PHP_EOL, (string) $html); + }); + } + + public static function markdownDataProvider() + { + yield ['[Laravel](https://laravel.com)', '

Laravel

']; + yield ['\[Laravel](https://laravel.com)', '

[Laravel](https://laravel.com)

']; + yield ['![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', '

Welcome to Laravel

']; + yield ['!\[Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)', '

![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)

']; + yield ['Visit https://laravel.com/docs to browse the documentation', '

Visit https://laravel.com/docs to browse the documentation

']; + yield ['Visit to browse the documentation', '

Visit https://laravel.com/docs to browse the documentation

']; + yield ['Visit https://laravel.com/docs to browse the documentation', '

Visit https://laravel.com/docs to browse the documentation

']; + } + + public static function markdownEncodedDataProvider() + { + yield [new EncodedHtmlString('[Laravel](https://laravel.com)'), '

[Laravel](https://laravel.com)

']; + + yield [ + new EncodedHtmlString('![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)'), + '

![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)

', + ]; + + yield [ + new EncodedHtmlString('Visit https://laravel.com/docs to browse the documentation'), + '

Visit https://laravel.com/docs to browse the documentation

', + ]; + + yield [ + new EncodedHtmlString('Visit to browse the documentation'), + '

Visit <https://laravel.com/docs> to browse the documentation

', + ]; + + yield [ + new EncodedHtmlString('Visit https://laravel.com/docs to browse the documentation'), + '

Visit <span>https://laravel.com/docs</span> to browse the documentation

', + ]; + + yield [ + new EncodedHtmlString(new HtmlString('Visit https://laravel.com/docs to browse the documentation')), + '

Visit https://laravel.com/docs to browse the documentation

', + ]; + + yield [ + '![Welcome to Laravel](https://laravel.com/assets/img/welcome/background.svg)
'.new EncodedHtmlString('Visit https://laravel.com/docs to browse the documentation'), + '

Welcome to Laravel
Visit <span>https://laravel.com/docs</span> to browse the documentation

', + ]; + } +} diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index eddfd83c23c1..441cb59dea97 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -166,6 +166,24 @@ public function testQueueMayBeNullForJobQueueingAndJobQueuedEvent() $this->assertNull($events[3]->queue); } + public function testQueuedClosureCanBeNamed() + { + Config::set('queue.default', 'database'); + $events = []; + $this->app['events']->listen(function (JobQueued $e) use (&$events) { + $events[] = $e; + }); + + dispatch(function () { + // + })->name('custom name'); + + $this->assertCount(1, $events); + $this->assertInstanceOf(JobQueued::class, $events[0]); + $this->assertSame('custom name', $events[0]->job->name); + $this->assertStringContainsString('custom name', $events[0]->job->displayName()); + } + public function testCanDisableDispatchingAfterResponse() { Job::dispatchAfterResponse('test'); diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index 334ec657536e..45116af9e3e8 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -138,6 +138,18 @@ public function testJobsCanHaveConditionalRateLimits() $this->assertJobWasReleased(NonAdminTestJob::class); } + public function testRateLimitedJobsCanBeSkippedOnLimitReachedAndReleasedAfter() + { + $rateLimiter = $this->app->make(RateLimiter::class); + + $rateLimiter->for('test', function ($job) { + return Limit::perHour(1); + }); + + $this->assertJobRanSuccessfully(RateLimitedReleaseAfterTestJob::class); + $this->assertJobWasReleasedAfter(RateLimitedReleaseAfterTestJob::class, 60); + } + public function testMiddlewareSerialization() { $rateLimited = new RateLimited('limiterName'); @@ -192,6 +204,25 @@ protected function assertJobWasReleased($class) $this->assertFalse($class::$handled); } + protected function assertJobWasReleasedAfter($class, $releaseAfter) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->once()->withArgs([$releaseAfter]); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertFalse($class::$handled); + } + protected function assertJobWasSkipped($class) { $class::$handled = false; @@ -341,6 +372,14 @@ public function middleware() } } +class RateLimitedReleaseAfterTestJob extends RateLimitedTestJob +{ + public function middleware() + { + return [(new RateLimited('test'))->releaseAfter(60)]; + } +} + enum BackedEnumNamedRateLimited: string { case FOO = 'bar'; diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index e559c41b7f8f..19f525afcc8d 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -40,6 +40,11 @@ public function testCircuitResetsAfterSuccess() $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); } + public function testCircuitCanSkipJob() + { + $this->assertJobWasDeleted(CircuitBreakerSkipJob::class); + } + protected function assertJobWasReleasedImmediately($class) { $class::$handled = false; @@ -82,6 +87,27 @@ protected function assertJobWasReleasedWithDelay($class) $this->assertFalse($class::$handled); } + protected function assertJobWasDeleted($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + $job->shouldReceive('isDeleted')->andReturn(true); + $job->shouldReceive('isReleased')->twice()->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } + protected function assertJobRanSuccessfully($class) { $class::$handled = false; @@ -314,6 +340,25 @@ public function middleware() } } +class CircuitBreakerSkipJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(Exception::class)]; + } +} + class CircuitBreakerSuccessfulJob { use InteractsWithQueue, Queueable; diff --git a/tests/Log/ContextTest.php b/tests/Log/ContextTest.php index dfb76df9194b..ef9d61f0bccc 100644 --- a/tests/Log/ContextTest.php +++ b/tests/Log/ContextTest.php @@ -364,6 +364,34 @@ public function test_it_can_retrieve_subset_of_context() ])); } + public function test_it_can_exclude_subset_of_context() + { + Context::add('parent.child.1', 5); + Context::add('parent.child.2', 6); + Context::add('another', 7); + + $this->assertSame([ + 'another' => 7, + ], Context::except([ + 'parent.child.1', + 'parent.child.2', + ])); + } + + public function test_it_can_exclude_subset_of_hidden_context() + { + Context::addHidden('parent.child.1', 5); + Context::addHidden('parent.child.2', 6); + Context::addHidden('another', 7); + + $this->assertSame([ + 'another' => 7, + ], Context::exceptHidden([ + 'parent.child.1', + 'parent.child.2', + ])); + } + public function test_it_adds_context_to_logging() { $path = storage_path('logs/laravel.log'); diff --git a/tests/Mail/MailMarkdownTest.php b/tests/Mail/MailMarkdownTest.php index 88ebf67893b3..cc4137d12dcd 100644 --- a/tests/Mail/MailMarkdownTest.php +++ b/tests/Mail/MailMarkdownTest.php @@ -3,6 +3,8 @@ namespace Illuminate\Tests\Mail; use Illuminate\Mail\Markdown; +use Illuminate\View\Compilers\BladeCompiler; +use Illuminate\View\Engines\EngineResolver; use Illuminate\View\Factory; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -17,6 +19,14 @@ protected function tearDown(): void public function testRenderFunctionReturnsHtml() { $viewFactory = m::mock(Factory::class); + $engineResolver = m::mock(EngineResolver::class); + $bladeCompiler = m::mock(BladeCompiler::class); + $viewFactory->shouldReceive('getEngineResolver')->andReturn($engineResolver); + $engineResolver->shouldReceive('resolve->getCompiler')->andReturn($bladeCompiler); + $bladeCompiler->shouldReceive('usingEchoFormat') + ->with('new \Illuminate\Support\EncodedHtmlString(%s)', m::type('Closure')) + ->andReturnUsing(fn ($echoFormat, $callback) => $callback()); + $markdown = new Markdown($viewFactory); $viewFactory->shouldReceive('flushFinderCache')->once(); $viewFactory->shouldReceive('replaceNamespace')->once()->with('mail', $markdown->htmlComponentPaths())->andReturnSelf(); @@ -33,6 +43,14 @@ public function testRenderFunctionReturnsHtml() public function testRenderFunctionReturnsHtmlWithCustomTheme() { $viewFactory = m::mock(Factory::class); + $engineResolver = m::mock(EngineResolver::class); + $bladeCompiler = m::mock(BladeCompiler::class); + $viewFactory->shouldReceive('getEngineResolver')->andReturn($engineResolver); + $engineResolver->shouldReceive('resolve->getCompiler')->andReturn($bladeCompiler); + $bladeCompiler->shouldReceive('usingEchoFormat') + ->with('new \Illuminate\Support\EncodedHtmlString(%s)', m::type('Closure')) + ->andReturnUsing(fn ($echoFormat, $callback) => $callback()); + $markdown = new Markdown($viewFactory); $markdown->theme('yaz'); $viewFactory->shouldReceive('flushFinderCache')->once(); @@ -50,6 +68,14 @@ public function testRenderFunctionReturnsHtmlWithCustomTheme() public function testRenderFunctionReturnsHtmlWithCustomThemeWithMailPrefix() { $viewFactory = m::mock(Factory::class); + $engineResolver = m::mock(EngineResolver::class); + $bladeCompiler = m::mock(BladeCompiler::class); + $viewFactory->shouldReceive('getEngineResolver')->andReturn($engineResolver); + $engineResolver->shouldReceive('resolve->getCompiler')->andReturn($bladeCompiler); + $bladeCompiler->shouldReceive('usingEchoFormat') + ->with('new \Illuminate\Support\EncodedHtmlString(%s)', m::type('Closure')) + ->andReturnUsing(fn ($echoFormat, $callback) => $callback()); + $markdown = new Markdown($viewFactory); $markdown->theme('mail.yaz'); $viewFactory->shouldReceive('flushFinderCache')->once(); diff --git a/tests/Queue/QueueBeanstalkdJobTest.php b/tests/Queue/QueueBeanstalkdJobTest.php index 514555001e67..1405cb3f0712 100755 --- a/tests/Queue/QueueBeanstalkdJobTest.php +++ b/tests/Queue/QueueBeanstalkdJobTest.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Jobs\BeanstalkdJob; +use Illuminate\Queue\Jobs\Job; use Mockery as m; use Pheanstalk\Contract\JobIdInterface; use Pheanstalk\Contract\PheanstalkManagerInterface; @@ -39,7 +40,7 @@ public function testFailProperlyCallsTheJobHandler() $job->getPheanstalkJob()->shouldReceive('getData')->andReturn(json_encode(['job' => 'foo', 'uuid' => 'test-uuid', 'data' => ['data']])); $job->getContainer()->shouldReceive('make')->once()->with('foo')->andReturn($handler = m::mock(BeanstalkdJobTestFailedTest::class)); $job->getPheanstalk()->shouldReceive('delete')->once()->with($job->getPheanstalkJob())->andReturnSelf(); - $handler->shouldReceive('failed')->once()->with(['data'], m::type(Exception::class), 'test-uuid'); + $handler->shouldReceive('failed')->once()->with(['data'], m::type(Exception::class), 'test-uuid', m::type(Job::class)); $job->getContainer()->shouldReceive('make')->once()->with(Dispatcher::class)->andReturn($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with(m::type(JobFailed::class))->andReturnNull(); diff --git a/tests/Queue/QueueSyncQueueTest.php b/tests/Queue/QueueSyncQueueTest.php index e7aab86ad779..901f66c2d75e 100755 --- a/tests/Queue/QueueSyncQueueTest.php +++ b/tests/Queue/QueueSyncQueueTest.php @@ -61,6 +61,28 @@ public function testFailedJobGetsHandledWhenAnExceptionIsThrown() Container::setInstance(); } + public function testFailedJobHasAccessToJobInstance() + { + unset($_SERVER['__sync.failed']); + + $sync = new SyncQueue; + $container = new Container; + $container->bind(\Illuminate\Contracts\Events\Dispatcher::class, \Illuminate\Events\Dispatcher::class); + $container->bind(\Illuminate\Contracts\Bus\Dispatcher::class, \Illuminate\Bus\Dispatcher::class); + $container->bind(\Illuminate\Contracts\Container\Container::class, \Illuminate\Container\Container::class); + $sync->setContainer($container); + + SyncQueue::createPayloadUsing(function ($connection, $queue, $payload) { + return ['data' => ['extra' => 'extraValue']]; + }); + + try { + $sync->push(new FailingSyncQueueJob()); + } catch (LogicException $e) { + $this->assertSame('extraValue', $_SERVER['__sync.failed']); + } + } + public function testCreatesPayloadObject() { $sync = new SyncQueue; @@ -177,6 +199,23 @@ public function failed() } } +class FailingSyncQueueJob implements ShouldQueue +{ + use InteractsWithQueue; + + public function handle() + { + throw new LogicException(); + } + + public function failed() + { + $payload = $this->job->payload(); + + $_SERVER['__sync.failed'] = $payload['data']['extra']; + } +} + class SyncQueueJob implements ShouldQueue { use InteractsWithQueue; diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index f03e4b10bb8c..54eab01a1cc8 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -1066,6 +1066,24 @@ public function testComplexRouteGenerationWithDefaultsAndBindingFields() $url->route('tenantSlugPost', ['post' => $keyParam('concretePost')]), ); + // Repeat the two assertions above without the 'tenant' default (without slug) + $url->defaults(['tenant' => null]); + + // tenantSlugPost: Tenant (with default) omitted, post passed positionally, with the default value for 'tenant' (without slug) removed + $this->assertSame( + 'https://www.foo.com/tenantSlugPost/defaultTenantSlug/concretePost', + $url->route('tenantSlugPost', [$keyParam('concretePost')]), + ); + + // tenantSlugPost: Tenant (with default) omitted, post passed using key, with the default value for 'tenant' (without slug) removed + $this->assertSame( + 'https://www.foo.com/tenantSlugPost/defaultTenantSlug/concretePost', + $url->route('tenantSlugPost', ['post' => $keyParam('concretePost')]), + ); + + // Revert the default value for the tenant parameter back + $url->defaults(['tenant' => 'defaultTenant']); + /** * One parameter with a default value, one without a default value. * diff --git a/tests/Support/Common.php b/tests/Support/Common.php new file mode 100644 index 000000000000..7928b8705624 --- /dev/null +++ b/tests/Support/Common.php @@ -0,0 +1,62 @@ + 'bar']; + } +} + +class TestJsonableObject implements Jsonable +{ + public function toJson($options = 0) + { + return '{"foo":"bar"}'; + } +} + +class TestJsonSerializeObject implements JsonSerializable +{ + public function jsonSerialize(): array + { + return ['foo' => 'bar']; + } +} + +class TestJsonSerializeWithScalarValueObject implements JsonSerializable +{ + public function jsonSerialize(): string + { + return 'foo'; + } +} + +class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable +{ + public $items; + + public function __construct($items = []) + { + $this->items = $items; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + public function jsonSerialize(): array + { + return json_decode(json_encode($this->items), true); + } +} diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index e5d15a06362f..a81e6eb79bee 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -11,6 +11,10 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use stdClass; +use WeakMap; + +include_once 'Common.php'; +include_once 'Enums.php'; class SupportArrTest extends TestCase { @@ -32,6 +36,25 @@ public function testAccessible(): void $this->assertFalse(Arr::accessible(static fn () => null)); } + public function testArrayable(): void + { + $this->assertTrue(Arr::arrayable([])); + $this->assertTrue(Arr::arrayable(new TestArrayableObject)); + $this->assertTrue(Arr::arrayable(new TestJsonableObject)); + $this->assertTrue(Arr::arrayable(new TestJsonSerializeObject)); + $this->assertTrue(Arr::arrayable(new TestTraversableAndJsonSerializableObject)); + + $this->assertFalse(Arr::arrayable(null)); + $this->assertFalse(Arr::arrayable('abc')); + $this->assertFalse(Arr::arrayable(new stdClass)); + $this->assertFalse(Arr::arrayable((object) ['a' => 1, 'b' => 2])); + $this->assertFalse(Arr::arrayable(123)); + $this->assertFalse(Arr::arrayable(12.34)); + $this->assertFalse(Arr::arrayable(true)); + $this->assertFalse(Arr::arrayable(new \DateTime)); + $this->assertFalse(Arr::arrayable(static fn () => null)); + } + public function testAdd() { $array = Arr::add(['name' => 'Desk'], 'price', 100); @@ -1485,6 +1508,32 @@ public function testForget() $this->assertEquals([2 => [1 => 'products']], $array); } + public function testFrom() + { + $this->assertSame(['foo' => 'bar'], Arr::from(['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], Arr::from((object) ['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestArrayableObject)); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonableObject)); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonSerializeObject)); + $this->assertSame(['foo'], Arr::from(new TestJsonSerializeWithScalarValueObject)); + + $this->assertSame(['name' => 'A'], Arr::from(TestEnum::A)); + $this->assertSame(['name' => 'A', 'value' => 1], Arr::from(TestBackedEnum::A)); + $this->assertSame(['name' => 'A', 'value' => 'A'], Arr::from(TestStringBackedEnum::A)); + + $subject = [new stdClass, new stdClass]; + $items = new TestTraversableAndJsonSerializableObject($subject); + $this->assertSame($subject, Arr::from($items)); + + $items = new WeakMap; + $items[$temp = new class {}] = 'bar'; + $this->assertSame(['bar'], Arr::from($items)); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Items cannot be represented by a scalar value.'); + Arr::from(123); + } + public function testWrap() { $string = 'a'; diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 2edd11ca6718..513e1fa4187a 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -8,7 +8,6 @@ use CachingIterator; use Exception; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; @@ -18,7 +17,6 @@ use Illuminate\Support\Str; use Illuminate\Support\Stringable; use InvalidArgumentException; -use IteratorAggregate; use JsonSerializable; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; @@ -26,10 +24,10 @@ use ReflectionClass; use stdClass; use Symfony\Component\VarDumper\VarDumper; -use Traversable; use UnexpectedValueException; use WeakMap; +include_once 'Common.php'; include_once 'Enums.php'; class SupportCollectionTest extends TestCase @@ -838,6 +836,10 @@ public function testContainsOneItem($collection) $this->assertFalse((new $collection([]))->containsOneItem()); $this->assertTrue((new $collection([1]))->containsOneItem()); $this->assertFalse((new $collection([1, 2]))->containsOneItem()); + + $this->assertFalse(collect([1, 2, 2])->containsOneItem(fn ($number) => $number === 2)); + $this->assertTrue(collect(['ant', 'bear', 'cat'])->containsOneItem(fn ($word) => strlen($word) === 4)); + $this->assertFalse(collect(['ant', 'bear', 'cat'])->containsOneItem(fn ($word) => strlen($word) > 4)); } public function testIterable() @@ -2911,14 +2913,12 @@ public function testConstructMethodFromObject($collection) #[DataProvider('collectionClassProvider')] public function testConstructMethodFromWeakMap($collection) { - $this->expectException('InvalidArgumentException'); - $map = new WeakMap(); $object = new stdClass; $object->foo = 'bar'; $map[$object] = 3; - $data = new $collection($map); + $this->assertEquals([3], $data->all()); } public function testSplice() @@ -5783,30 +5783,6 @@ public function offsetUnset($offset): void } } -class TestArrayableObject implements Arrayable -{ - public function toArray() - { - return ['foo' => 'bar']; - } -} - -class TestJsonableObject implements Jsonable -{ - public function toJson($options = 0) - { - return '{"foo":"bar"}'; - } -} - -class TestJsonSerializeObject implements JsonSerializable -{ - public function jsonSerialize(): array - { - return ['foo' => 'bar']; - } -} - class TestJsonSerializeToStringObject implements JsonSerializable { public function jsonSerialize(): string @@ -5815,34 +5791,6 @@ public function jsonSerialize(): string } } -class TestJsonSerializeWithScalarValueObject implements JsonSerializable -{ - public function jsonSerialize(): string - { - return 'foo'; - } -} - -class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable -{ - public $items; - - public function __construct($items) - { - $this->items = $items; - } - - public function getIterator(): Traversable - { - return new ArrayIterator($this->items); - } - - public function jsonSerialize(): array - { - return json_decode(json_encode($this->items), true); - } -} - class TestCollectionMapIntoObject { public $value; diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index ee67f818a3cd..1074e97dd6d9 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -8,6 +8,7 @@ use Error; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Env; use Illuminate\Support\Optional; use Illuminate\Support\Sleep; @@ -26,10 +27,21 @@ class SupportHelpersTest extends TestCase { + protected function setUp(): void + { + mkdir(__DIR__.'/tmp'); + + parent::setUp(); + } + protected function tearDown(): void { m::close(); + if (is_dir(__DIR__.'/tmp')) { + (new Filesystem)->deleteDirectory(__DIR__.'/tmp'); + } + parent::tearDown(); } @@ -747,11 +759,13 @@ class_uses_recursive(SupportTestClassThree::class) public function testTraitUsesRecursive() { - $this->assertSame([ - 'Illuminate\Tests\Support\SupportTestTraitTwo' => 'Illuminate\Tests\Support\SupportTestTraitTwo', - 'Illuminate\Tests\Support\SupportTestTraitOne' => 'Illuminate\Tests\Support\SupportTestTraitOne', - ], - trait_uses_recursive(SupportTestClassOne::class)); + $this->assertSame( + [ + 'Illuminate\Tests\Support\SupportTestTraitTwo' => 'Illuminate\Tests\Support\SupportTestTraitTwo', + 'Illuminate\Tests\Support\SupportTestTraitOne' => 'Illuminate\Tests\Support\SupportTestTraitOne', + ], + trait_uses_recursive(SupportTestClassOne::class) + ); $this->assertSame([], trait_uses_recursive(SupportTestClassTwo::class)); } @@ -1212,6 +1226,232 @@ public function testEnvEscapedString() $this->assertSame('x"null"x', env('foo')); } + public function testWriteArrayOfEnvVariablesToFile() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + 'DB_PORT' => 3306, + 'BRAND_NEW_PREFIX' => 'fresh value', + ], $path); + + $this->assertSame( + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST="127:0:0:1"', + 'DB_PORT=3306', + '', + 'BRAND_NEW_PREFIX="fresh value"', + ]), + $filesystem->get($path) + ); + } + + public function testWriteArrayOfEnvVariablesToFileAndOverwrite() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + 'DB_CONNECTION' => 'sqlite', + ], $path, true); + + $this->assertSame( + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=sqlite', + 'DB_HOST="127:0:0:1"', + ]), + $filesystem->get($path) + ); + } + + public function testWillNotOverwriteArrayOfVariables() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + ], $path); + + $this->assertSame( + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST="127:0:0:1"', + ]), + $filesystem->get($path) + ); + } + + public function testWriteVariableToFile() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); + + Env::writeVariable('APP_VIBE', 'chill', $path); + + $this->assertSame( + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ]), + $filesystem->get($path) + ); + } + + public function testWillNotOverwriteVariable() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); + + Env::writeVariable('APP_VIBE', 'chill', $path); + + $this->assertSame( + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ]), + $filesystem->get($path) + ); + } + + public function testWriteVariableToFileAndOverwrite() + { + $filesystem = new Filesystem; + $path = __DIR__.'/tmp/env-test-file'; + $filesystem->put($path, implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=odd', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ])); + + Env::writeVariable('APP_VIBE', 'chill', $path, true); + + $this->assertSame( + implode(PHP_EOL, [ + 'APP_NAME=Laravel', + 'APP_ENV=local', + 'APP_KEY=base64:randomkey', + 'APP_DEBUG=true', + 'APP_URL=http://localhost', + 'APP_VIBE=chill', + '', + 'DB_CONNECTION=mysql', + 'DB_HOST=', + ]), + $filesystem->get($path) + ); + } + + public function testWillThrowAnExceptionIfFileIsMissingWhenTryingToWriteVariables(): void + { + $this->expectExceptionObject(new RuntimeException('The file [missing-file] does not exist.')); + + Env::writeVariables([ + 'APP_VIBE' => 'chill', + 'DB_HOST' => '127:0:0:1', + ], 'missing-file'); + } + public function testGetFromSERVERFirst() { $_ENV['foo'] = 'From $_ENV'; diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 8f7567433baa..7f7de7f3f1a2 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -335,4 +335,39 @@ public function testTrim() $this->assertSame(12.3456789, Number::trim(12.3456789)); $this->assertSame(12.3456789, Number::trim(12.34567890000)); } + + #[RequiresPhpExtension('intl')] + public function testParse() + { + $this->assertSame(1234.0, Number::parse('1,234')); + $this->assertSame(1234.5, Number::parse('1,234.5')); + $this->assertSame(1234.56, Number::parse('1,234.56')); + $this->assertSame(-1234.56, Number::parse('-1,234.56')); + + $this->assertSame(1234.56, Number::parse('1.234,56', locale: 'de')); + $this->assertSame(1234.56, Number::parse('1 234,56', locale: 'fr')); + } + + #[RequiresPhpExtension('intl')] + public function testParseInt() + { + $this->assertSame(1234, Number::parseInt('1,234')); + $this->assertSame(1234, Number::parseInt('1,234.5')); + $this->assertSame(-1234, Number::parseInt('-1,234.56')); + + $this->assertSame(1234, Number::parseInt('1.234', locale: 'de')); + $this->assertSame(1234, Number::parseInt('1 234', locale: 'fr')); + } + + #[RequiresPhpExtension('intl')] + public function testParseFloat() + { + $this->assertSame(1234.0, Number::parseFloat('1,234')); + $this->assertSame(1234.5, Number::parseFloat('1,234.5')); + $this->assertSame(1234.56, Number::parseFloat('1,234.56')); + $this->assertSame(-1234.56, Number::parseFloat('-1,234.56')); + + $this->assertSame(1234.56, Number::parseFloat('1.234,56', locale: 'de')); + $this->assertSame(1234.56, Number::parseFloat('1 234,56', locale: 'fr')); + } } diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index cae497bdd133..f9389a82e516 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -1086,6 +1086,18 @@ public function testAssertUnprocessable() $response->assertUnprocessable(); } + public function testAssertClientError() + { + $statusCode = 400; + + $baseResponse = tap(new Response, function ($response) use ($statusCode) { + $response->setStatusCode($statusCode); + }); + + $response = TestResponse::fromBaseResponse($baseResponse); + $response->assertClientError(); + } + public function testAssertServerError() { $statusCode = 500; @@ -2652,6 +2664,27 @@ public function testAssertRedirect() $response->assertRedirect(); } + public function testAssertRedirectBack() + { + app()->instance('session.store', $store = new Store('test-session', new ArraySessionHandler(1))); + + $store->setPreviousUrl('https://url.com'); + + app('url')->setSessionResolver(fn () => app('session.store')); + + $response = TestResponse::fromBaseResponse( + (new Response('', 302))->withHeaders(['Location' => 'https://url.com']) + ); + + $response->assertRedirectBack(); + + $this->expectException(ExpectationFailedException::class); + + $store->setPreviousUrl('https://url.net'); + + $response->assertRedirectBack(); + } + public function testGetDecryptedCookie() { $response = TestResponse::fromBaseResponse( diff --git a/types/Http/Request.php b/types/Http/Request.php index 340b77bb973f..c95bed31cf70 100644 --- a/types/Http/Request.php +++ b/types/Http/Request.php @@ -14,3 +14,9 @@ enum TestEnum: string ]); assertType('TestEnum|null', $request->enum('key', TestEnum::class)); + +assertType('Illuminate\Routing\Route', $request->route()); +assertType('object|string|null', $request->route('key')); + +assertType('Symfony\Component\HttpFoundation\InputBag', $request->json()); +assertType('mixed', $request->json('key'));